mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
refactor settings API entrypoints
This commit is contained in:
parent
10a9038dcc
commit
ebb8bf7add
157 changed files with 2318 additions and 2408 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import type { PaginationMeta } from "@qltysh/fabro-api-client";
|
||||
|
||||
/**
|
||||
* Opaque settings payload returned by `/api/v1/runs/:id/settings`. Mirrors the
|
||||
* v2 `SettingsFile` shape in `lib/crates/fabro-types/src/settings/tree.rs`,
|
||||
* with secret-bearing subtrees dropped before serialization. Treated as a
|
||||
* loose JSON object on the web side — consumers only render it.
|
||||
* Opaque persisted `SettingsLayer` payload returned by `/api/v1/runs/:id/settings`.
|
||||
* Treated as a loose JSON object on the web side — consumers only render it.
|
||||
*/
|
||||
export type RunSettings = Record<string, unknown>;
|
||||
export type RunSettingsLayer = Record<string, unknown>;
|
||||
|
||||
export interface WorkflowScheduleSummary {
|
||||
expression: string;
|
||||
|
|
@ -35,6 +33,6 @@ export interface WorkflowDetailResponse {
|
|||
slug: string;
|
||||
description: string;
|
||||
filename: string;
|
||||
settings: RunSettings;
|
||||
settings: RunSettingsLayer;
|
||||
graph: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ import { apiJson } from "../api";
|
|||
import { isVisibleStage } from "../data/runs";
|
||||
import { formatDurationSecs } from "../lib/format";
|
||||
import type { PaginatedRunStageList } from "@qltysh/fabro-api-client";
|
||||
import type { RunSettings } from "../lib/workflow-api";
|
||||
import type { RunSettingsLayer } from "../lib/workflow-api";
|
||||
|
||||
export const handle = { wide: true };
|
||||
|
||||
export async function loader({ request, params }: any) {
|
||||
const [{ data: apiStages }, settings] = await Promise.all([
|
||||
apiJson<PaginatedRunStageList>(`/runs/${params.id}/stages`, { request }),
|
||||
apiJson<RunSettings>(`/runs/${params.id}/settings`, { request }),
|
||||
apiJson<RunSettingsLayer>(`/runs/${params.id}/settings`, { request }),
|
||||
]);
|
||||
const stages: Stage[] = apiStages.filter((s) => isVisibleStage(s.id)).map((s) => ({
|
||||
id: s.id,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
import type { ServerSettings } from "@qltysh/fabro-api-client";
|
||||
import { apiJson } from "../api";
|
||||
import { CollapsibleFile } from "../components/collapsible-file";
|
||||
|
||||
/**
|
||||
* Opaque server settings payload returned by `/api/v1/settings`. Mirrors the
|
||||
* v2 `SettingsFile` shape with secret-bearing subtrees dropped before
|
||||
* serialization. The UI only renders it as JSON.
|
||||
*/
|
||||
type ServerSettings = Record<string, unknown>;
|
||||
|
||||
export function meta({}: any) {
|
||||
return [{ title: "Settings — Fabro" }];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
import { ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import { Link, Outlet, useLocation, useParams } from "react-router";
|
||||
import { apiJsonOrNull } from "../api";
|
||||
import type { RunSettings, WorkflowDetailResponse as ApiWorkflowDetail } from "../lib/workflow-api";
|
||||
import type {
|
||||
RunSettingsLayer,
|
||||
WorkflowDetailResponse as ApiWorkflowDetail,
|
||||
} from "../lib/workflow-api";
|
||||
|
||||
export interface WorkflowEntry {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
filename: string;
|
||||
settings: RunSettings;
|
||||
settings: RunSettingsLayer;
|
||||
graph: string;
|
||||
}
|
||||
|
||||
// Static sample data used by the `workflow-definition` index route for the
|
||||
// hardcoded showcase workflows. Shape mirrors the v2 `SettingsFile` JSON
|
||||
// returned by `/api/v1/runs/:id/settings` (see the Rust
|
||||
// `fabro_types::settings::SettingsFile` type). Fields are opaque to the
|
||||
// `RunSettings` TypeScript type, which is a bare `Record<string, unknown>`.
|
||||
// hardcoded showcase workflows. Shape mirrors the persisted `SettingsLayer`
|
||||
// JSON returned by `/api/v1/runs/:id/settings`. Fields are opaque to the
|
||||
// `RunSettingsLayer` TypeScript type, which is a bare `Record<string, unknown>`.
|
||||
export const workflowData: Record<string, WorkflowEntry> = {
|
||||
fix_build: {
|
||||
name: "Fix Build",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
78
lib/crates/fabro-api/tests/server_settings_round_trip.rs
Normal file
78
lib/crates/fabro-api/tests/server_settings_round_trip.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::{
|
||||
FeaturesNamespace as ApiFeaturesNamespace, ObjectStoreSettings as ApiObjectStoreSettings,
|
||||
ServerNamespace as ApiServerNamespace, ServerSettings as ApiServerSettings,
|
||||
};
|
||||
use fabro_config::{ServerSettings, parse_settings_layer};
|
||||
use fabro_types::settings::server::ObjectStoreSettings;
|
||||
use fabro_types::settings::{FeaturesNamespace, ServerNamespace};
|
||||
|
||||
#[test]
|
||||
fn server_settings_family_reuses_domain_types() {
|
||||
assert_same_type::<ApiServerSettings, ServerSettings>();
|
||||
assert_same_type::<ApiServerNamespace, ServerNamespace>();
|
||||
assert_same_type::<ApiFeaturesNamespace, FeaturesNamespace>();
|
||||
assert_same_type::<ApiObjectStoreSettings, ObjectStoreSettings>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_settings_json_matches_openapi_shape() {
|
||||
let layer = parse_settings_layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.api]
|
||||
url = "https://api.fabro.example.com"
|
||||
|
||||
[server.web]
|
||||
enabled = true
|
||||
url = "https://fabro.example.com"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token", "github"]
|
||||
|
||||
[server.auth.github]
|
||||
allowed_usernames = ["alice"]
|
||||
|
||||
[server.storage]
|
||||
root = "/srv/fabro"
|
||||
|
||||
[server.integrations.github]
|
||||
enabled = true
|
||||
strategy = "app"
|
||||
app_id = "12345"
|
||||
client_id = "Iv1.abcdef"
|
||||
slug = "fabro-dev"
|
||||
|
||||
[features]
|
||||
session_sandboxes = true
|
||||
"#,
|
||||
)
|
||||
.expect("settings fixture should parse");
|
||||
let settings = ServerSettings::from_layer(&layer).expect("settings should resolve");
|
||||
|
||||
let json = serde_json::to_value(&settings).expect("server settings should serialize");
|
||||
assert_eq!(json["server"]["listen"]["type"], "tcp");
|
||||
assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276");
|
||||
assert_eq!(json["server"]["storage"]["root"], "/srv/fabro");
|
||||
assert_eq!(json["features"]["session_sandboxes"], true);
|
||||
|
||||
let round_trip: ApiServerSettings =
|
||||
serde_json::from_value(json).expect("server settings should deserialize");
|
||||
assert_eq!(round_trip, settings);
|
||||
}
|
||||
|
||||
fn assert_same_type<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -712,13 +712,6 @@ pub(crate) struct SystemEventsArgs {
|
|||
pub(crate) struct SettingsArgs {
|
||||
#[command(flatten)]
|
||||
pub(crate) target: ServerTargetArgs,
|
||||
|
||||
/// Show only locally resolved settings and skip the server call
|
||||
#[arg(long, conflicts_with = "server")]
|
||||
pub(crate) local: bool,
|
||||
|
||||
/// Optional workflow name, .fabro path, or .toml run config to overlay
|
||||
pub(crate) workflow: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use fabro_config::UserSettings;
|
||||
use fabro_config::merge::combine_files;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_types::settings::{CliSettings, SettingsLayer};
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
|
|
@ -33,7 +34,8 @@ pub(crate) struct CommandContext {
|
|||
cwd: PathBuf,
|
||||
base_config_path: PathBuf,
|
||||
machine_settings: SettingsLayer,
|
||||
cli_settings: CliSettings,
|
||||
user_settings: UserSettings,
|
||||
cli_settings: CliNamespace,
|
||||
server_mode: ServerMode,
|
||||
server: OnceCell<Arc<Client>>,
|
||||
}
|
||||
|
|
@ -41,7 +43,7 @@ pub(crate) struct CommandContext {
|
|||
impl CommandContext {
|
||||
pub(crate) fn base(
|
||||
printer: Printer,
|
||||
cli_settings: CliSettings,
|
||||
cli_settings: CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
Self::new(printer, ServerMode::None, cli_settings, cli_layer)
|
||||
|
|
@ -50,7 +52,7 @@ impl CommandContext {
|
|||
pub(crate) fn for_target(
|
||||
args: &ServerTargetArgs,
|
||||
printer: Printer,
|
||||
cli_settings: CliSettings,
|
||||
cli_settings: CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
|
|
@ -66,7 +68,7 @@ impl CommandContext {
|
|||
pub(crate) fn for_connection(
|
||||
args: &ServerConnectionArgs,
|
||||
printer: Printer,
|
||||
cli_settings: CliSettings,
|
||||
cli_settings: CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
Self::new(
|
||||
|
|
@ -83,7 +85,7 @@ impl CommandContext {
|
|||
fn new(
|
||||
printer: Printer,
|
||||
server_mode: ServerMode,
|
||||
cli_settings: CliSettings,
|
||||
cli_settings: CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
) -> Result<Self> {
|
||||
let cwd = std::env::current_dir().context("Failed to get current directory")?;
|
||||
|
|
@ -99,12 +101,14 @@ impl CommandContext {
|
|||
cli: Some(cli_layer.clone()),
|
||||
..SettingsLayer::default()
|
||||
});
|
||||
let user_settings = user_config::resolve_user_settings(&machine_settings)?;
|
||||
|
||||
Ok(Self {
|
||||
printer,
|
||||
cwd,
|
||||
base_config_path,
|
||||
machine_settings,
|
||||
user_settings,
|
||||
cli_settings,
|
||||
server_mode,
|
||||
server: OnceCell::new(),
|
||||
|
|
@ -123,15 +127,15 @@ impl CommandContext {
|
|||
&self.cwd
|
||||
}
|
||||
|
||||
pub(crate) fn base_config_path(&self) -> &Path {
|
||||
&self.base_config_path
|
||||
}
|
||||
|
||||
pub(crate) fn machine_settings(&self) -> &SettingsLayer {
|
||||
&self.machine_settings
|
||||
}
|
||||
|
||||
pub(crate) fn cli_settings(&self) -> &CliSettings {
|
||||
pub(crate) fn user_settings(&self) -> &UserSettings {
|
||||
&self.user_settings
|
||||
}
|
||||
|
||||
pub(crate) fn cli_settings(&self) -> &CliNamespace {
|
||||
&self.cli_settings
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ mod cp;
|
|||
mod list;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_types::{RunId, StageId};
|
||||
use fabro_util::printer::Printer;
|
||||
|
|
@ -26,7 +26,7 @@ pub(super) async fn resolve_artifacts(
|
|||
run_selector: &str,
|
||||
node: Option<&str>,
|
||||
retry: Option<u32>,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<(RunId, Client, Vec<ArtifactEntry>)> {
|
||||
|
|
@ -65,7 +65,7 @@ pub(super) async fn resolve_artifacts(
|
|||
|
||||
pub(crate) async fn dispatch(
|
||||
ns: ArtifactNamespace,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,151 +8,41 @@
|
|||
)]
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_config::effective_settings::{
|
||||
EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer,
|
||||
};
|
||||
use fabro_config::{load_settings_project, project};
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::settings::{CliSettings, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use serde_json::json;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::args::SettingsArgs;
|
||||
use crate::command_context::CommandContext;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config;
|
||||
|
||||
fn config_layers(
|
||||
ctx: &CommandContext,
|
||||
workflow: Option<&Path>,
|
||||
) -> anyhow::Result<EffectiveSettingsLayers> {
|
||||
let cwd = ctx.cwd();
|
||||
let (workflow_layer, project_layer) = match workflow {
|
||||
Some(path) => workflow_and_project_layers(path, cwd)?,
|
||||
None => (SettingsLayer::default(), load_settings_project(cwd)?),
|
||||
};
|
||||
let user_layer =
|
||||
user_config::load_settings_with_config_and_storage_dir(Some(ctx.base_config_path()), None)?;
|
||||
Ok(EffectiveSettingsLayers::new(
|
||||
SettingsLayer::default(),
|
||||
workflow_layer,
|
||||
project_layer,
|
||||
user_layer,
|
||||
))
|
||||
}
|
||||
|
||||
fn workflow_and_project_layers(
|
||||
path: &Path,
|
||||
cwd: &Path,
|
||||
) -> anyhow::Result<(SettingsLayer, SettingsLayer)> {
|
||||
let resolution = project::resolve_workflow_path(path, cwd)?;
|
||||
if resolution.workflow_config.is_none() && !resolution.resolved_workflow_path.is_file() {
|
||||
anyhow::bail!(
|
||||
"Workflow not found: {}",
|
||||
resolution.resolved_workflow_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let workflow_layer = resolution.workflow_config.unwrap_or_default();
|
||||
let project_layer = project::discover_project_config(
|
||||
resolution
|
||||
.resolved_workflow_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(".")),
|
||||
)?
|
||||
.map(|(_, config)| config)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok((workflow_layer, project_layer))
|
||||
}
|
||||
|
||||
fn strip_nulls(value: &mut serde_json::Value) {
|
||||
match value {
|
||||
serde_json::Value::Object(map) => {
|
||||
for child in map.values_mut() {
|
||||
strip_nulls(child);
|
||||
}
|
||||
map.retain(|_, child| !child.is_null());
|
||||
}
|
||||
serde_json::Value::Array(values) => {
|
||||
for child in values {
|
||||
strip_nulls(child);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_settings_value(
|
||||
args: &SettingsArgs,
|
||||
cli: &CliSettings,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
let base_ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
|
||||
let layers = config_layers(&base_ctx, args.workflow.as_deref())?;
|
||||
let local_settings =
|
||||
materialize_settings_layer(layers, None, EffectiveSettingsMode::LocalOnly)?;
|
||||
let mut value = resolve_local_settings_value(&local_settings)?;
|
||||
strip_nulls(&mut value);
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn render_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
|
||||
anyhow::anyhow!(
|
||||
"failed to resolve local settings:\n{}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_local_settings_value(file: &SettingsLayer) -> anyhow::Result<serde_json::Value> {
|
||||
let file = fabro_config::apply_builtin_defaults(file.clone());
|
||||
|
||||
let project = fabro_config::resolve_project_from_file(&file).map_err(render_resolve_errors)?;
|
||||
let workflow =
|
||||
fabro_config::resolve_workflow_from_file(&file).map_err(render_resolve_errors)?;
|
||||
let run = fabro_config::resolve_run_from_file(&file).map_err(render_resolve_errors)?;
|
||||
let cli = fabro_config::resolve_cli_from_file(&file).map_err(render_resolve_errors)?;
|
||||
let features =
|
||||
fabro_config::resolve_features_from_file(&file).map_err(render_resolve_errors)?;
|
||||
|
||||
Ok(json!({
|
||||
"project": project,
|
||||
"workflow": workflow,
|
||||
"run": run,
|
||||
"cli": cli,
|
||||
"features": features,
|
||||
}))
|
||||
#[derive(Serialize)]
|
||||
struct RenderedConfig {
|
||||
user: fabro_config::UserSettings,
|
||||
server: fabro_api::types::ServerSettings,
|
||||
}
|
||||
|
||||
async fn rendered_config(
|
||||
args: &SettingsArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<serde_json::Value> {
|
||||
if args.local {
|
||||
return local_settings_value(args, cli, cli_layer, printer);
|
||||
}
|
||||
if args.workflow.is_some() {
|
||||
anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`");
|
||||
}
|
||||
let ctx = CommandContext::for_target(&args.target, printer, cli.clone(), cli_layer)?;
|
||||
ctx.server()
|
||||
let user = fabro_config::UserSettings::resolve()?;
|
||||
let server = ctx
|
||||
.server()
|
||||
.await?
|
||||
.retrieve_resolved_server_settings()
|
||||
.await
|
||||
.await?;
|
||||
serde_json::to_value(RenderedConfig { user, server }).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(
|
||||
args: &SettingsArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> anyhow::Result<()> {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||
use anyhow::Result;
|
||||
use fabro_api::types as api_types;
|
||||
use fabro_config::user::active_settings_path;
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
pub(crate) use fabro_util::check_report::{
|
||||
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
|
||||
|
|
@ -143,7 +143,7 @@ fn render_report(report: &CheckReport, styles: &Styles, verbose: bool, printer:
|
|||
pub(crate) async fn run_doctor(
|
||||
args: &DoctorArgs,
|
||||
verbose: bool,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<i32, anyhow::Error> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ use fabro_auth::{AuthCredential, AuthMethod, codex_oauth_config, credential_id_f
|
|||
use fabro_config::bind::Bind;
|
||||
use fabro_config::daemon::ServerDaemon;
|
||||
use fabro_config::user::{SETTINGS_CONFIG_FILENAME, default_storage_dir};
|
||||
use fabro_config::{ResolveError, Storage, envfile};
|
||||
use fabro_config::{Storage, envfile};
|
||||
use fabro_install::{
|
||||
InstallListenConfig, generate_jwt_keypair, merge_server_settings as merge_server_settings_impl,
|
||||
write_github_app_settings, write_token_settings,
|
||||
|
|
@ -34,7 +34,7 @@ use fabro_server::serve;
|
|||
use fabro_store::ArtifactStore;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_types::settings::server::ServerAuthMethod;
|
||||
use fabro_types::settings::{CliSettings, SettingsLayer};
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
|
|
@ -1273,24 +1273,13 @@ fn persist_github_install_changes(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn render_server_resolve_errors(errors: Vec<ResolveError>) -> anyhow::Error {
|
||||
anyhow::anyhow!(
|
||||
"failed to resolve server settings:\n{}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
async fn write_artifact_store_metadata(
|
||||
settings: &SettingsLayer,
|
||||
fabro_version: &str,
|
||||
) -> Result<()> {
|
||||
let resolved =
|
||||
fabro_config::resolve_server_from_file(settings).map_err(render_server_resolve_errors)?;
|
||||
let (object_store, prefix) = serve::build_artifact_object_store(&resolved)?;
|
||||
fabro_config::ServerSettings::from_layer(settings).map_err(anyhow::Error::from)?;
|
||||
let (object_store, prefix) = serve::build_artifact_object_store(&resolved.server)?;
|
||||
let artifact_store = ArtifactStore::new(object_store, prefix);
|
||||
artifact_store.write_metadata(fabro_version).await?;
|
||||
Ok(())
|
||||
|
|
@ -1427,7 +1416,7 @@ where
|
|||
pub(crate) async fn execute(
|
||||
args: &InstallArgs,
|
||||
command: Option<InstallCommand>,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
|
|
@ -1443,7 +1432,7 @@ pub(crate) async fn execute(
|
|||
async fn run_install_github_command(
|
||||
args: &InstallArgs,
|
||||
github_args: &InstallGithubArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
@ -1606,7 +1595,7 @@ async fn run_install_github_inner(
|
|||
|
||||
pub(crate) async fn run_install(
|
||||
args: &InstallArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
process_local_json: bool,
|
||||
printer: Printer,
|
||||
|
|
@ -1632,7 +1621,7 @@ pub(crate) async fn run_install(
|
|||
|
||||
async fn run_install_inner(
|
||||
args: &InstallArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
@ -1808,8 +1797,7 @@ async fn run_install_inner(
|
|||
.context("failed to parse generated settings.toml")?,
|
||||
args.storage_dir.as_deref(),
|
||||
);
|
||||
fabro_config::resolve_server_from_file(&install_settings)
|
||||
.map_err(render_server_resolve_errors)?;
|
||||
fabro_config::ServerSettings::from_layer(&install_settings).map_err(anyhow::Error::from)?;
|
||||
|
||||
// Secrets and auth material
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use cli_table::format::{Border, Justify, Separator};
|
|||
use cli_table::{Cell, CellStruct, Color, Style, Table};
|
||||
use fabro_api::types as api_types;
|
||||
use fabro_model::{Catalog, Model, Provider};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -42,7 +42,7 @@ struct ModelTestOutput {
|
|||
|
||||
pub(crate) async fn execute(
|
||||
command: Option<ModelsCommand>,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use fabro_config::Storage;
|
|||
use fabro_github::GitHubCredentials;
|
||||
use fabro_types::PullRequestRecord;
|
||||
use fabro_types::settings::cli::CliLayer;
|
||||
use fabro_types::settings::{CliSettings, InterpString};
|
||||
use fabro_types::settings::{CliNamespace, InterpString};
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
use crate::args::{PrCommand, PrNamespace, ServerTargetArgs};
|
||||
|
|
@ -22,7 +22,7 @@ const GITHUB_CREDENTIALS_REQUIRED: &str =
|
|||
|
||||
pub(crate) async fn dispatch(
|
||||
ns: PrNamespace,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
@ -42,28 +42,20 @@ pub(crate) async fn dispatch(
|
|||
reason = "boundary-exempt(pr-api): remove with follow-up #1 when PR ops move server-side"
|
||||
)]
|
||||
fn load_github_credentials_required(
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<GitHubCredentials> {
|
||||
let ctx = CommandContext::base(printer, cli.clone(), cli_layer)?;
|
||||
let server_settings =
|
||||
fabro_config::resolve_server_from_file(ctx.machine_settings()).map_err(|errors| {
|
||||
anyhow!(
|
||||
"failed to resolve server settings:\n{}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
})?;
|
||||
let server_settings = fabro_config::ServerSettings::from_layer(ctx.machine_settings())
|
||||
.map_err(anyhow::Error::from)?;
|
||||
let vault = user_config::storage_dir(ctx.machine_settings())
|
||||
.ok()
|
||||
.and_then(|dir| fabro_vault::Vault::load(Storage::new(&dir).secrets_path()).ok());
|
||||
let creds = build_github_credentials(
|
||||
server_settings.integrations.github.strategy,
|
||||
server_settings.server.integrations.github.strategy,
|
||||
server_settings
|
||||
.server
|
||||
.integrations
|
||||
.github
|
||||
.app_id
|
||||
|
|
@ -79,7 +71,7 @@ fn load_github_credentials_required(
|
|||
pub(crate) async fn load_pr_record(
|
||||
server: &ServerTargetArgs,
|
||||
run_id: &str,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<(PullRequestRecord, fabro_types::RunId)> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
|
||||
pub(crate) fn run_deinit(cli: &CliSettings, printer: Printer) -> Result<Vec<String>> {
|
||||
pub(crate) fn run_deinit(cli: &CliNamespace, printer: Printer) -> Result<Vec<String>> {
|
||||
let repo_root = super::init::git_repo_root()?;
|
||||
let mut removed = Vec::new();
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
|
|
@ -36,7 +36,7 @@ pub(super) fn git_repo_root() -> Result<PathBuf> {
|
|||
|
||||
pub(crate) async fn run_init(
|
||||
args: &RepoInitArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<Vec<String>> {
|
||||
|
|
@ -159,7 +159,7 @@ draft = true
|
|||
|
||||
async fn check_github_app_installation(
|
||||
target: &ServerTargetArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ use anyhow::Result;
|
|||
use fabro_api::types;
|
||||
use fabro_interview::{AnswerValue, ConsoleInterviewer, Question, QuestionOption, QuestionType};
|
||||
use fabro_store::EventEnvelope;
|
||||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::run::ApprovalMode;
|
||||
use fabro_types::{EventBody, RunId};
|
||||
use fabro_util::json::normalize_json_value;
|
||||
|
|
@ -49,6 +48,7 @@ pub(crate) async fn attach_run(
|
|||
kill_on_detach: bool,
|
||||
styles: &'static Styles,
|
||||
json_output: bool,
|
||||
live_verbose: bool,
|
||||
) -> Result<ExitCode> {
|
||||
let inferred_storage_dir = infer_storage_dir(run_dir);
|
||||
let inferred_run_id = infer_run_id(run_dir);
|
||||
|
|
@ -63,6 +63,7 @@ pub(crate) async fn attach_run(
|
|||
kill_on_detach,
|
||||
styles,
|
||||
json_output,
|
||||
live_verbose,
|
||||
Printer::Default,
|
||||
))
|
||||
.await;
|
||||
|
|
@ -79,6 +80,7 @@ pub(crate) async fn attach_run_with_client(
|
|||
kill_on_detach: bool,
|
||||
styles: &'static Styles,
|
||||
json_output: bool,
|
||||
live_verbose: bool,
|
||||
printer: Printer,
|
||||
) -> Result<ExitCode> {
|
||||
let state = client.get_run_state(run_id).await?;
|
||||
|
|
@ -86,10 +88,6 @@ pub(crate) async fn attach_run_with_client(
|
|||
fabro_config::resolve_run_from_file(&record.settings)
|
||||
.is_ok_and(|settings| settings.execution.approval == ApprovalMode::Auto)
|
||||
});
|
||||
let verbose = state.spec.as_ref().is_some_and(|record| {
|
||||
fabro_config::resolve_cli_from_file(&record.settings)
|
||||
.is_ok_and(|settings| settings.output.verbosity == OutputVerbosity::Verbose)
|
||||
});
|
||||
let events = client.list_run_events(run_id, None, None).await?;
|
||||
let replay_events = events.clone();
|
||||
let next_seq = events.last().map_or(1, |event| event.seq.saturating_add(1));
|
||||
|
|
@ -98,7 +96,7 @@ pub(crate) async fn attach_run_with_client(
|
|||
|
||||
if state_is_terminal(&state) || initial_exit_code.is_some() {
|
||||
return replay_run_with_client(
|
||||
verbose,
|
||||
live_verbose,
|
||||
events,
|
||||
initial_exit_code
|
||||
.or(state_exit_code)
|
||||
|
|
@ -116,7 +114,7 @@ pub(crate) async fn attach_run_with_client(
|
|||
styles,
|
||||
AttachOptions {
|
||||
auto_approve,
|
||||
verbose,
|
||||
verbose: live_verbose,
|
||||
kill_on_detach,
|
||||
json_output,
|
||||
},
|
||||
|
|
@ -542,6 +540,7 @@ mod tests {
|
|||
false,
|
||||
no_color_styles(),
|
||||
false,
|
||||
false,
|
||||
))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::{CliLayer, OutputFormat};
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::fs;
|
||||
|
|
@ -28,7 +28,7 @@ enum CopyDirection {
|
|||
|
||||
pub(crate) async fn cp_command(
|
||||
args: CpArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
|
|
@ -128,7 +128,7 @@ fn parse_direction(src: &str, dst: &str) -> Result<CopyDirection> {
|
|||
async fn resolve_client_and_run_id(
|
||||
server: &ServerTargetArgs,
|
||||
run_prefix: &str,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
cli_layer: &CliLayer,
|
||||
printer: Printer,
|
||||
) -> Result<(Client, fabro_types::RunId)> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
|||
use chrono::{DateTime, Utc};
|
||||
use cli_table::format::{Border, Separator};
|
||||
use cli_table::{Cell, CellStruct, Style, Table};
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -25,7 +25,7 @@ fn format_age(dt: DateTime<Utc>, now: DateTime<Utc>) -> String {
|
|||
pub(super) async fn list_command(
|
||||
client: &Client,
|
||||
_args: &SecretListArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let secrets = client.list_secrets().await?;
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use std::io::{IsTerminal, Read as _};
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use fabro_api::types;
|
||||
use fabro_types::settings::CliSettings;
|
||||
use fabro_types::settings::CliNamespace;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_util::printer::Printer;
|
||||
use tokio::task::spawn_blocking;
|
||||
|
|
@ -60,7 +60,7 @@ async fn resolve_value(args: &SecretSetArgs) -> Result<String> {
|
|||
pub(super) async fn set_command(
|
||||
client: &Client,
|
||||
args: &SecretSetArgs,
|
||||
cli: &CliSettings,
|
||||
cli: &CliNamespace,
|
||||
printer: Printer,
|
||||
) -> Result<()> {
|
||||
let value = resolve_value(args).await?;
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ pub(crate) fn bind_request(
|
|||
}
|
||||
|
||||
pub(crate) fn auth_methods(settings: &SettingsLayer) -> Vec<ServerAuthMethod> {
|
||||
fabro_config::resolve_server_from_file(settings)
|
||||
.map(|resolved| resolved.auth.methods)
|
||||
fabro_config::ServerSettings::from_layer(settings)
|
||||
.map(|resolved| resolved.server.auth.methods)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use anyhow::Result;
|
|||
pub(crate) use fabro_client::ServerTarget;
|
||||
pub(crate) use fabro_config::user::*;
|
||||
use fabro_types::settings::cli::CliTargetSettings;
|
||||
use fabro_types::settings::{CliSettings, SettingsLayer};
|
||||
use fabro_types::settings::{CliNamespace, SettingsLayer};
|
||||
use fabro_util::version::FABRO_VERSION;
|
||||
use tracing::debug;
|
||||
|
||||
|
|
@ -30,19 +30,14 @@ pub(crate) fn load_settings_with_config_and_storage_dir(
|
|||
Ok(apply_storage_dir_override(layer, storage_dir))
|
||||
}
|
||||
|
||||
fn render_resolve_errors(errors: Vec<fabro_config::ResolveError>) -> anyhow::Error {
|
||||
anyhow::anyhow!(
|
||||
"failed to resolve cli settings:\n{}",
|
||||
errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
pub(crate) fn resolve_user_settings(
|
||||
file: &SettingsLayer,
|
||||
) -> anyhow::Result<fabro_config::UserSettings> {
|
||||
fabro_config::UserSettings::from_layer(file).map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result<CliSettings> {
|
||||
fabro_config::resolve_cli_from_file(file).map_err(render_resolve_errors)
|
||||
pub(crate) fn resolve_cli_settings(file: &SettingsLayer) -> anyhow::Result<CliNamespace> {
|
||||
resolve_user_settings(file).map(|settings| settings.cli)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_storage_dir_override(
|
||||
|
|
@ -64,7 +59,7 @@ pub(crate) fn apply_storage_dir_override(
|
|||
|
||||
/// Pull the resolved CLI target configuration out of `[cli.target]`.
|
||||
/// Returns either an http(s) URL or a unix socket path.
|
||||
fn cli_target_from_settings(settings: &CliSettings) -> Option<String> {
|
||||
fn cli_target_from_settings(settings: &CliNamespace) -> Option<String> {
|
||||
let target = settings.target.as_ref()?;
|
||||
match target {
|
||||
CliTargetSettings::Http { url } => Some(url.as_source()),
|
||||
|
|
@ -73,8 +68,8 @@ fn cli_target_from_settings(settings: &CliSettings) -> Option<String> {
|
|||
}
|
||||
|
||||
fn configured_server_target(settings: &SettingsLayer) -> Result<Option<ServerTarget>> {
|
||||
let cli_settings = resolve_cli_settings(settings)?;
|
||||
let Some(value) = cli_target_from_settings(&cli_settings) else {
|
||||
let user_settings = resolve_user_settings(settings)?;
|
||||
let Some(value) = cli_target_from_settings(&user_settings.cli) else {
|
||||
return Ok(None);
|
||||
};
|
||||
parse_server_target(&value).map(Some)
|
||||
|
|
|
|||
|
|
@ -40,93 +40,10 @@ fn parse_settings(stdout: &[u8]) -> serde_json::Value {
|
|||
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML settings")
|
||||
}
|
||||
|
||||
fn parse_settings_json(stdout: &[u8]) -> serde_json::Value {
|
||||
serde_json::from_slice(stdout).expect("stdout should be valid JSON settings")
|
||||
}
|
||||
|
||||
fn run_goal_inline(settings: &serde_json::Value) -> Option<&str> {
|
||||
let goal = settings.get("run")?.get("goal")?;
|
||||
(goal.get("type")?.as_str() == Some("inline"))
|
||||
.then(|| goal.get("value")?.as_str())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn run_model_name(settings: &serde_json::Value) -> Option<&str> {
|
||||
settings.get("run")?.get("model")?.get("name")?.as_str()
|
||||
}
|
||||
|
||||
fn run_model_provider(settings: &serde_json::Value) -> Option<&str> {
|
||||
settings.get("run")?.get("model")?.get("provider")?.as_str()
|
||||
}
|
||||
|
||||
fn run_inputs(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("inputs"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.expect("run.inputs")
|
||||
}
|
||||
|
||||
fn run_sandbox(settings: &serde_json::Value) -> &serde_json::Value {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("sandbox"))
|
||||
.expect("run.sandbox")
|
||||
}
|
||||
|
||||
fn run_checkpoint(settings: &serde_json::Value) -> &serde_json::Value {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("checkpoint"))
|
||||
.expect("run.checkpoint")
|
||||
}
|
||||
|
||||
fn run_hooks(settings: &serde_json::Value) -> &[serde_json::Value] {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("hooks"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.expect("run.hooks")
|
||||
}
|
||||
|
||||
fn run_agent_mcps(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("agent"))
|
||||
.and_then(|agent| agent.get("mcps"))
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.expect("run.agent.mcps")
|
||||
}
|
||||
|
||||
fn auto_approve_enabled(settings: &serde_json::Value) -> bool {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("execution"))
|
||||
.and_then(|execution| execution.get("approval"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some("auto")
|
||||
}
|
||||
|
||||
fn run_prepare_commands(settings: &serde_json::Value) -> Vec<String> {
|
||||
settings
|
||||
.get("run")
|
||||
.and_then(|run| run.get("prepare"))
|
||||
.and_then(|prepare| prepare.get("commands"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.expect("run.prepare.commands")
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value
|
||||
.as_str()
|
||||
.expect("command should be a string")
|
||||
.to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn server_storage_root(settings: &serde_json::Value) -> &str {
|
||||
settings
|
||||
.get("server")
|
||||
.and_then(|server| server.get("server"))
|
||||
.and_then(|server| server.get("storage"))
|
||||
.and_then(|storage| storage.get("root"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
|
|
@ -157,7 +74,7 @@ shared = "server"
|
|||
}
|
||||
|
||||
fn resolved_server_settings_fixture() -> serde_json::Value {
|
||||
let settings = fabro_config::resolve(&server_settings_layer_fixture())
|
||||
let settings = fabro_config::ServerSettings::from_layer(&server_settings_layer_fixture())
|
||||
.expect("server settings fixture should resolve");
|
||||
serde_json::to_value(settings).expect("resolved settings payload should serialize")
|
||||
}
|
||||
|
|
@ -389,155 +306,6 @@ script = "workflow-setup"
|
|||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn settings_local_merges_cli_and_project_defaults() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
|
||||
let output = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let cfg = parse_settings(&output);
|
||||
assert!(cfg.get("_version").is_none());
|
||||
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
|
||||
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
|
||||
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
|
||||
assert_eq!(cfg["run"]["sandbox"]["provider"].as_str(), Some("daytona"));
|
||||
assert_eq!(run_model_name(&cfg), Some("project-model"));
|
||||
assert_eq!(run_model_provider(&cfg), Some("openai"));
|
||||
assert_eq!(run_goal_inline(&cfg), None);
|
||||
|
||||
// v2 R22: run.inputs replaces the inherited map wholesale rather than
|
||||
// merging by key, so the project layer wipes out the CLI layer's inputs.
|
||||
let vars = run_inputs(&cfg);
|
||||
assert_eq!(
|
||||
vars.get("project_only").and_then(serde_json::Value::as_str),
|
||||
Some("1")
|
||||
);
|
||||
assert_eq!(
|
||||
vars.get("shared").and_then(serde_json::Value::as_str),
|
||||
Some("project")
|
||||
);
|
||||
assert!(
|
||||
!vars.contains_key("cli_only"),
|
||||
"run.inputs should replace across layers, not merge by key"
|
||||
);
|
||||
|
||||
// v2 R71: provider-native maps such as run.sandbox.daytona.labels remain
|
||||
// sticky merge-by-key, so CLI labels persist under the project layer.
|
||||
let sandbox = run_sandbox(&cfg);
|
||||
let labels = &sandbox["daytona"]["labels"];
|
||||
assert_eq!(labels["cli_only"].as_str(), Some("1"));
|
||||
assert_eq!(labels["shared"].as_str(), Some("cli"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
|
||||
let output = context
|
||||
.settings()
|
||||
.current_dir(project.path())
|
||||
.args(["--local", "demo"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let cfg = parse_settings(&output);
|
||||
assert_eq!(run_goal_inline(&cfg), Some("demo goal"));
|
||||
assert_eq!(run_model_name(&cfg), Some("run-model"));
|
||||
assert_eq!(run_model_provider(&cfg), Some("anthropic"));
|
||||
|
||||
// v2 R22: run.inputs replaces wholesale, so the workflow layer wins
|
||||
// over project and cli.
|
||||
let vars = run_inputs(&cfg);
|
||||
assert_eq!(vars.get("run_only").and_then(|v| v.as_str()), Some("1"));
|
||||
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("run"));
|
||||
|
||||
// checkpoint.exclude_globs is a security/policy list: replace by default.
|
||||
let checkpoint = run_checkpoint(&cfg);
|
||||
assert_eq!(
|
||||
checkpoint["exclude_globs"],
|
||||
serde_json::json!(["run-only", "shared"])
|
||||
);
|
||||
|
||||
// Hooks: id-based replacement. The "shared" hook appears in both cli and
|
||||
// workflow layers and resolves to the workflow entry; project and run-only
|
||||
// contribute the other two ids.
|
||||
let hooks = run_hooks(&cfg);
|
||||
assert!(hooks.len() >= 2);
|
||||
let shared_hook = hooks
|
||||
.iter()
|
||||
.find(|hook| hook["name"].as_str() == Some("shared"))
|
||||
.expect("shared hook");
|
||||
assert_eq!(shared_hook["command"].as_str(), Some("echo run"));
|
||||
assert!(
|
||||
hooks
|
||||
.iter()
|
||||
.any(|hook| hook["name"].as_str() == Some("run-only"))
|
||||
);
|
||||
|
||||
let mcps = run_agent_mcps(&cfg);
|
||||
let shared = mcps.get("shared").expect("shared mcp");
|
||||
assert_eq!(shared["transport"]["type"].as_str(), Some("stdio"));
|
||||
assert_eq!(
|
||||
shared["transport"]["command"],
|
||||
serde_json::json!(["echo", "run"])
|
||||
);
|
||||
assert!(mcps.contains_key("run_only"));
|
||||
|
||||
// run.sandbox.daytona.labels stays sticky merge-by-key per R71.
|
||||
let sandbox = run_sandbox(&cfg);
|
||||
let labels = &sandbox["daytona"]["labels"];
|
||||
assert_eq!(labels["run_only"].as_str(), Some("1"));
|
||||
assert_eq!(labels["shared"].as_str(), Some("run"));
|
||||
|
||||
// run.sandbox.env stays sticky merge-by-key per R71.
|
||||
let env = &sandbox["env"];
|
||||
assert_eq!(env["CLI_ONLY"].as_str(), Some("1"));
|
||||
assert_eq!(env["RUN_ONLY"].as_str(), Some("1"));
|
||||
assert_eq!(env["SHARED"].as_str(), Some("run"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_local_explicit_workflow_path_uses_workflow_project_layers() {
|
||||
let mut context = test_context!();
|
||||
let (project, _storage_dir) = setup_external_workflow_fixture(&mut context);
|
||||
let cwd = tempfile::tempdir().unwrap();
|
||||
let workflow = project.path().join("workflow.toml");
|
||||
|
||||
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
|
||||
let output = context
|
||||
.settings()
|
||||
.env_remove("FABRO_STORAGE_DIR")
|
||||
.current_dir(cwd.path())
|
||||
.args(["--local", workflow.to_str().unwrap()])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let cfg = parse_settings(&output);
|
||||
assert!(auto_approve_enabled(&cfg));
|
||||
// v2 R30: run.prepare.steps replaces the whole ordered list across layers.
|
||||
// The highest-precedence layer (workflow) wins.
|
||||
assert_eq!(run_prepare_commands(&cfg), vec![
|
||||
"workflow-setup".to_string()
|
||||
]);
|
||||
assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
|
||||
let mut context = test_context!();
|
||||
|
|
@ -608,254 +376,6 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_fabro_path_matches_ambient_defaults() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
|
||||
let ambient = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
let graph = context
|
||||
.settings()
|
||||
.current_dir(project.path())
|
||||
.args(["--local", "standalone.fabro"])
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
assert_eq!(parse_settings(&graph), parse_settings(&ambient));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_missing_run_config_errors() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
|
||||
let mut cmd = context.settings();
|
||||
cmd.current_dir(project.path());
|
||||
cmd.args(["--local", "missing.toml"]);
|
||||
let output = cmd.output().expect("command should execute");
|
||||
assert!(!output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(
|
||||
stderr.contains("workflow not found:"),
|
||||
"stderr should report missing workflow path, got:\n{stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains("missing.toml"),
|
||||
"stderr should include missing workflow filename, got:\n{stderr}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_legacy_cli_config_is_silently_ignored() {
|
||||
let context = test_context!();
|
||||
let project = tempfile::tempdir().unwrap();
|
||||
|
||||
context.write_home(
|
||||
".fabro/cli.toml",
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[run.model]
|
||||
name = "legacy-model"
|
||||
"#,
|
||||
);
|
||||
|
||||
let assert = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(
|
||||
assert.get_output().stderr.is_empty(),
|
||||
"settings should not warn about legacy config files: {}",
|
||||
String::from_utf8_lossy(&assert.get_output().stderr)
|
||||
);
|
||||
|
||||
let cfg = parse_settings(&assert.get_output().stdout);
|
||||
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
|
||||
assert!(
|
||||
cfg["run"]["model"].get("name").is_none(),
|
||||
"resolved dense settings should omit an unset run.model.name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_legacy_user_config_is_silently_ignored() {
|
||||
let context = test_context!();
|
||||
let project = tempfile::tempdir().unwrap();
|
||||
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[run.model]
|
||||
name = "legacy-model"
|
||||
"#,
|
||||
);
|
||||
|
||||
let assert = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(
|
||||
assert.get_output().stderr.is_empty(),
|
||||
"settings should not warn about legacy config files: {}",
|
||||
String::from_utf8_lossy(&assert.get_output().stderr)
|
||||
);
|
||||
|
||||
let cfg = parse_settings(&assert.get_output().stdout);
|
||||
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
|
||||
assert!(
|
||||
cfg["run"]["model"].get("name").is_none(),
|
||||
"resolved dense settings should omit an unset run.model.name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_legacy_server_config_is_silently_ignored() {
|
||||
let context = test_context!();
|
||||
let project = tempfile::tempdir().unwrap();
|
||||
|
||||
context.write_home(
|
||||
".fabro/server.toml",
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[run.model]
|
||||
name = "legacy-model"
|
||||
"#,
|
||||
);
|
||||
|
||||
let assert = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(
|
||||
assert.get_output().stderr.is_empty(),
|
||||
"settings should not warn about legacy config files: {}",
|
||||
String::from_utf8_lossy(&assert.get_output().stderr)
|
||||
);
|
||||
|
||||
let cfg = parse_settings(&assert.get_output().stdout);
|
||||
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
|
||||
assert!(
|
||||
cfg["run"]["model"].get("name").is_none(),
|
||||
"resolved dense settings should omit an unset run.model.name"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_user_config_wins_over_legacy_cli_config() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
context.write_home(
|
||||
".fabro/cli.toml",
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.model]
|
||||
name = "legacy-model"
|
||||
|
||||
[run.inputs]
|
||||
shared = "legacy"
|
||||
"#,
|
||||
);
|
||||
|
||||
let assert = context
|
||||
.settings()
|
||||
.arg("--local")
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert!(
|
||||
assert.get_output().stderr.is_empty(),
|
||||
"settings should not warn about legacy config files: {}",
|
||||
String::from_utf8_lossy(&assert.get_output().stderr)
|
||||
);
|
||||
|
||||
let cfg = parse_settings(&assert.get_output().stdout);
|
||||
assert_eq!(run_model_name(&cfg), Some("project-model"));
|
||||
let vars = run_inputs(&cfg);
|
||||
assert_eq!(
|
||||
vars.get("shared").and_then(serde_json::Value::as_str),
|
||||
Some("project")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_uses_fabro_home_for_home_config_resolution() {
|
||||
let context = test_context!();
|
||||
let fabro_home = tempfile::tempdir().unwrap();
|
||||
|
||||
std::fs::write(
|
||||
fabro_home.path().join("settings.toml"),
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
|
||||
[run.model]
|
||||
name = "from-fabro-home"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = context
|
||||
.settings()
|
||||
.args(["--local", "--json"])
|
||||
.env("FABRO_HOME", fabro_home.path())
|
||||
.env_remove("FABRO_STORAGE_DIR")
|
||||
.output()
|
||||
.expect("command should execute");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"settings command failed:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
|
||||
let cfg = parse_settings_json(&output.stdout);
|
||||
assert!(cfg.get("_version").is_none());
|
||||
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose"));
|
||||
assert_eq!(
|
||||
cfg["run"]["model"]["name"].as_str(),
|
||||
Some("from-fabro-home")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_rejects_server_url_flag() {
|
||||
let context = test_context!();
|
||||
|
|
@ -883,32 +403,27 @@ fn settings_rejects_storage_dir_flag() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn settings_rejects_local_and_server_combination() {
|
||||
fn settings_rejects_local_flag() {
|
||||
let context = test_context!();
|
||||
context
|
||||
.settings()
|
||||
.args(["--local", "--server", "https://cli.example.com"])
|
||||
.arg("--local")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"the argument '--local' cannot be used with '--server <SERVER>'",
|
||||
"unexpected argument '--local' found",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_rejects_workflow_without_local() {
|
||||
fn settings_rejects_workflow_argument() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
|
||||
context
|
||||
.settings()
|
||||
.current_dir(project.path())
|
||||
.arg("demo")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"WORKFLOW requires --local; use `fabro settings --local WORKFLOW`",
|
||||
));
|
||||
.stderr(predicate::str::contains("unexpected argument 'demo' found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -918,12 +433,9 @@ fn settings_fetches_server_resolved_settings() {
|
|||
let server = MockServer::start();
|
||||
let server_settings = resolved_server_settings_fixture();
|
||||
let mock = server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/settings")
|
||||
.query_param("view", "resolved");
|
||||
when.method("GET").path("/api/v1/settings");
|
||||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Fabro-Settings-View", "resolved")
|
||||
.body(server_settings_body(&server_settings));
|
||||
});
|
||||
context.write_home(
|
||||
|
|
@ -962,30 +474,25 @@ shared = "cli"
|
|||
|
||||
mock.assert();
|
||||
let cfg = parse_settings(&output);
|
||||
assert!(cfg.get("_version").is_none());
|
||||
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
|
||||
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
|
||||
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
|
||||
assert_eq!(run_model_name(&cfg), Some("server-model"));
|
||||
assert_eq!(run_model_provider(&cfg), Some("openai"));
|
||||
assert_eq!(
|
||||
cfg["user"]["cli"]["output"]["verbosity"].as_str(),
|
||||
Some("verbose")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg["user"]["features"]["session_sandboxes"].as_bool(),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
cfg["server"]["server"]["auth"]["methods"][0].as_str(),
|
||||
Some("dev-token")
|
||||
);
|
||||
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
|
||||
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
|
||||
|
||||
// Server-backed mode now returns the selected server's own dense resolved
|
||||
// settings; local project/user overlays are not merged into the output.
|
||||
let vars = run_inputs(&cfg);
|
||||
assert_eq!(
|
||||
vars.get("server_only").and_then(serde_json::Value::as_str),
|
||||
Some("1")
|
||||
);
|
||||
assert_eq!(
|
||||
vars.get("shared").and_then(serde_json::Value::as_str),
|
||||
Some("server")
|
||||
);
|
||||
assert!(
|
||||
!vars.contains_key("project_only"),
|
||||
"server-backed settings output must not include local workflow/project overlays"
|
||||
cfg["server"]["server"]["artifacts"]["store"]["type"].as_str(),
|
||||
Some("local")
|
||||
);
|
||||
assert!(cfg.get("run").is_none());
|
||||
assert!(cfg.get("project").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -994,21 +501,16 @@ fn settings_cli_server_target_overrides_configured_server_target() {
|
|||
let project = setup_settings_fixture(&context);
|
||||
let configured_server = MockServer::start();
|
||||
let configured_mock = configured_server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/settings")
|
||||
.query_param("view", "resolved");
|
||||
when.method("GET").path("/api/v1/settings");
|
||||
then.status(500)
|
||||
.body("configured-server-should-not-be-used");
|
||||
});
|
||||
let cli_server = MockServer::start();
|
||||
let cli_server_settings = resolved_server_settings_fixture();
|
||||
let cli_mock = cli_server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/settings")
|
||||
.query_param("view", "resolved");
|
||||
when.method("GET").path("/api/v1/settings");
|
||||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Fabro-Settings-View", "resolved")
|
||||
.body(server_settings_body(&cli_server_settings));
|
||||
});
|
||||
context.write_home(
|
||||
|
|
@ -1044,46 +546,6 @@ verbosity = "verbose"
|
|||
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_errors_when_server_lacks_resolved_view_marker() {
|
||||
let context = test_context!();
|
||||
let project = setup_settings_fixture(&context);
|
||||
let server = MockServer::start();
|
||||
let server_settings = resolved_server_settings_fixture();
|
||||
let mock = server.mock(|when, then| {
|
||||
when.method("GET")
|
||||
.path("/api/v1/settings")
|
||||
.query_param("view", "resolved");
|
||||
then.status(200)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(server_settings_body(&server_settings));
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[cli.target]
|
||||
type = "http"
|
||||
url = "{}/api/v1"
|
||||
"#,
|
||||
server.base_url()
|
||||
),
|
||||
);
|
||||
|
||||
context
|
||||
.settings()
|
||||
.current_dir(project.path())
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"server does not support resolved settings view; upgrade the server or use --local",
|
||||
));
|
||||
|
||||
mock.assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_unreachable_http_target_fails_clearly() {
|
||||
let context = test_context!();
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -490,26 +490,18 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn retrieve_resolved_server_settings(&self) -> Result<serde_json::Value> {
|
||||
let url = format!("{}/api/v1/settings?view=resolved", self.base_url());
|
||||
pub async fn retrieve_resolved_server_settings(
|
||||
&self,
|
||||
) -> Result<fabro_api::types::ServerSettings> {
|
||||
let url = format!("{}/api/v1/settings", self.base_url());
|
||||
let response = self
|
||||
.send_http(|http_client| async move { http_client.get(&url).send().await })
|
||||
.await?;
|
||||
|
||||
let marker = response
|
||||
.headers()
|
||||
.get("x-fabro-settings-view")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
if marker != Some("resolved") {
|
||||
bail!(
|
||||
"server does not support resolved settings view; upgrade the server or use --local"
|
||||
);
|
||||
}
|
||||
|
||||
response
|
||||
.json::<serde_json::Value>()
|
||||
.json::<fabro_api::types::ServerSettings>()
|
||||
.await
|
||||
.context("server returned invalid JSON for the resolved settings view")
|
||||
.context("server returned invalid JSON for server settings")
|
||||
}
|
||||
|
||||
pub async fn create_run_from_manifest(&self, manifest: types::RunManifest) -> Result<RunId> {
|
||||
|
|
|
|||
|
|
@ -37,3 +37,4 @@ ulid.workspace = true
|
|||
[dev-dependencies]
|
||||
toml.workspace = true
|
||||
fabro-types = { path = "../fabro-types", features = ["test-support"] }
|
||||
temp-env = "0.3"
|
||||
|
|
|
|||
60
lib/crates/fabro-config/src/context.rs
Normal file
60
lib/crates/fabro-config/src/context.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use fabro_types::settings::{CliNamespace, FeaturesNamespace, ServerNamespace, SettingsLayer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::resolve::{resolve_cli, resolve_features, resolve_server};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{Error, Result, apply_builtin_defaults};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ServerSettings {
|
||||
pub server: ServerNamespace,
|
||||
pub features: FeaturesNamespace,
|
||||
}
|
||||
|
||||
impl ServerSettings {
|
||||
pub fn from_layer(layer: &SettingsLayer) -> Result<Self> {
|
||||
let layer = apply_builtin_defaults(layer.clone());
|
||||
let mut errors = Vec::new();
|
||||
let server_layer = layer.server.clone().unwrap_or_default();
|
||||
let features_layer = layer.features.clone().unwrap_or_default();
|
||||
let server = resolve_server(&server_layer, &mut errors);
|
||||
let features = resolve_features(&features_layer, &mut errors);
|
||||
if errors.is_empty() {
|
||||
Ok(Self { server, features })
|
||||
} else {
|
||||
Err(Error::resolve("failed to resolve server settings", errors))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve() -> Result<Self> {
|
||||
let layer = load_settings_config(None)?;
|
||||
Self::from_layer(&layer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct UserSettings {
|
||||
pub cli: CliNamespace,
|
||||
pub features: FeaturesNamespace,
|
||||
}
|
||||
|
||||
impl UserSettings {
|
||||
pub fn from_layer(layer: &SettingsLayer) -> Result<Self> {
|
||||
let layer = apply_builtin_defaults(layer.clone());
|
||||
let mut errors = Vec::new();
|
||||
let cli_layer = layer.cli.clone().unwrap_or_default();
|
||||
let features_layer = layer.features.clone().unwrap_or_default();
|
||||
let cli = resolve_cli(&cli_layer, &mut errors);
|
||||
let features = resolve_features(&features_layer, &mut errors);
|
||||
if errors.is_empty() {
|
||||
Ok(Self { cli, features })
|
||||
} else {
|
||||
Err(Error::resolve("failed to resolve user settings", errors))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve() -> Result<Self> {
|
||||
let layer = load_settings_config(None)?;
|
||||
Self::from_layer(&layer)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
//! across all three config files (settings.toml, .fabro/project.toml,
|
||||
//! workflow.toml).
|
||||
//! Owner-specific domains (`cli`, `server`) are consumed only from the local
|
||||
//! `~/.fabro/settings.toml` plus explicit process-local overrides — their
|
||||
//! `~/.fabro/settings.toml` plus explicit process-local overrides. Their
|
||||
//! stanzas in `.fabro/project.toml` and `workflow.toml` remain schema-valid but
|
||||
//! inert.
|
||||
|
||||
|
|
@ -16,13 +16,6 @@ use fabro_types::settings::server::ServerLayer;
|
|||
use crate::merge::combine_files;
|
||||
use crate::{Error, Result, apply_builtin_defaults};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EffectiveSettingsMode {
|
||||
LocalOnly,
|
||||
RemoteServer,
|
||||
LocalDaemon,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct EffectiveSettingsLayers {
|
||||
pub args: SettingsLayer,
|
||||
|
|
@ -53,7 +46,6 @@ impl EffectiveSettingsLayers {
|
|||
pub fn materialize_settings_layer(
|
||||
layers: EffectiveSettingsLayers,
|
||||
server_settings: Option<&SettingsLayer>,
|
||||
mode: EffectiveSettingsMode,
|
||||
) -> Result<SettingsLayer> {
|
||||
let EffectiveSettingsLayers {
|
||||
args,
|
||||
|
|
@ -61,47 +53,28 @@ pub fn materialize_settings_layer(
|
|||
mut project,
|
||||
user,
|
||||
} = layers;
|
||||
let server_settings = server_settings.ok_or(Error::MissingServerSettings)?;
|
||||
|
||||
let settings = match mode {
|
||||
EffectiveSettingsMode::LocalOnly => {
|
||||
combine_files(combine_files(combine_files(user, project), workflow), args)
|
||||
}
|
||||
EffectiveSettingsMode::RemoteServer | EffectiveSettingsMode::LocalDaemon => {
|
||||
let server_settings = server_settings.ok_or(Error::MissingServerSettings)?;
|
||||
// Owner-specific domains (cli, server) may only come from the
|
||||
// local ~/.fabro/settings.toml, never from .fabro/project.toml or
|
||||
// workflow.toml. The user layer keeps its cli/server fields.
|
||||
strip_owner_domains(&mut workflow);
|
||||
strip_owner_domains(&mut project);
|
||||
// Owner-specific domains (cli, server) may only come from the local
|
||||
// ~/.fabro/settings.toml, never from .fabro/project.toml or workflow.toml.
|
||||
// The user layer keeps its cli/server fields.
|
||||
strip_owner_domains(&mut workflow);
|
||||
strip_owner_domains(&mut project);
|
||||
|
||||
let server_defaults = server_settings.clone();
|
||||
let combined = combine_files(combine_files(combine_files(user, project), workflow), args);
|
||||
let mut settings = enforce_server_authority(combined, server_settings);
|
||||
|
||||
let combined =
|
||||
combine_files(combine_files(combine_files(user, project), workflow), args);
|
||||
|
||||
let mut settings = match mode {
|
||||
EffectiveSettingsMode::RemoteServer => {
|
||||
apply_server_defaults(combined, &server_defaults)
|
||||
}
|
||||
EffectiveSettingsMode::LocalDaemon => {
|
||||
apply_local_daemon_overrides(combined, &server_defaults)
|
||||
}
|
||||
EffectiveSettingsMode::LocalOnly => unreachable!(),
|
||||
};
|
||||
// Storage root always comes from the server's local
|
||||
// ~/.fabro/settings.toml, never from the client.
|
||||
if let Some(server_root) = server_settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|s| s.storage.as_ref())
|
||||
.cloned()
|
||||
{
|
||||
let server = settings.server.get_or_insert_with(ServerLayer::default);
|
||||
server.storage = Some(server_root);
|
||||
}
|
||||
settings
|
||||
}
|
||||
};
|
||||
// Storage root always comes from the server's local ~/.fabro/settings.toml,
|
||||
// never from the client.
|
||||
if let Some(server_root) = server_settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.storage.as_ref())
|
||||
.cloned()
|
||||
{
|
||||
let server = settings.server.get_or_insert_with(ServerLayer::default);
|
||||
server.storage = Some(server_root);
|
||||
}
|
||||
|
||||
Ok(apply_builtin_defaults(settings))
|
||||
}
|
||||
|
|
@ -111,30 +84,11 @@ fn strip_owner_domains(file: &mut SettingsLayer) {
|
|||
file.server = None;
|
||||
}
|
||||
|
||||
/// Apply server-side defaults to a client-layered [`SettingsLayer`].
|
||||
/// Enforce server-owned fields on a client-layered [`SettingsLayer`].
|
||||
///
|
||||
/// Server-owned domains (`server`, `features`, and parts of `run`) flow from
|
||||
/// the server's local `~/.fabro/settings.toml` when the corresponding client
|
||||
/// value is absent. Run-shaped defaults (model, prepare, sandbox, checkpoint,
|
||||
/// hooks, agent mcps, etc.) also flow from server to client so the persisted
|
||||
/// run spec matches the server's local configuration.
|
||||
fn apply_server_defaults(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer {
|
||||
// Server-owned domains: server-side always wins when client left blank.
|
||||
// Use the v2 merge matrix with the server layer in lower precedence so
|
||||
// that client-supplied values still dominate when present.
|
||||
settings = combine_files(server.clone(), settings);
|
||||
settings
|
||||
}
|
||||
|
||||
/// Apply server-side overrides in LocalDaemon mode.
|
||||
///
|
||||
/// In LocalDaemon mode, a subset of server-owned fields unconditionally
|
||||
/// override any client-side values. Client-controlled run-level fields are
|
||||
/// left alone.
|
||||
fn apply_local_daemon_overrides(
|
||||
mut settings: SettingsLayer,
|
||||
server: &SettingsLayer,
|
||||
) -> SettingsLayer {
|
||||
/// A subset of server-owned fields unconditionally override any client-side
|
||||
/// values. Client-controlled run-level fields are left alone.
|
||||
fn enforce_server_authority(mut settings: SettingsLayer, server: &SettingsLayer) -> SettingsLayer {
|
||||
if let Some(server_layer) = server.server.clone() {
|
||||
let client = settings.server.get_or_insert_with(ServerLayer::default);
|
||||
if let Some(storage) = server_layer.storage {
|
||||
|
|
@ -173,7 +127,7 @@ mod tests {
|
|||
use fabro_types::settings::server::{ServerLayer, ServerSchedulerLayer, ServerStorageLayer};
|
||||
use fabro_types::settings::{InterpString, SettingsLayer};
|
||||
|
||||
use super::{EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer};
|
||||
use super::{EffectiveSettingsLayers, materialize_settings_layer};
|
||||
use crate::parse::parse_settings_layer;
|
||||
|
||||
fn layer(source: &str) -> SettingsLayer {
|
||||
|
|
@ -181,7 +135,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn local_only_merges_project_and_user_layers() {
|
||||
fn materialize_settings_layer_merges_layers_and_applies_server_authority() {
|
||||
let settings = materialize_settings_layer(
|
||||
EffectiveSettingsLayers::new(
|
||||
SettingsLayer::default(),
|
||||
|
|
@ -214,8 +168,17 @@ shared = "user"
|
|||
"#,
|
||||
),
|
||||
),
|
||||
None,
|
||||
EffectiveSettingsMode::LocalOnly,
|
||||
Some(&layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.storage]
|
||||
root = "/srv/fabro"
|
||||
|
||||
[server.scheduler]
|
||||
max_concurrent_runs = 7
|
||||
"#,
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -229,7 +192,7 @@ shared = "user"
|
|||
.as_deref(),
|
||||
Some("project-model")
|
||||
);
|
||||
// Per R22, run.inputs replaces wholesale — the winning layer is the
|
||||
// Per R22, run.inputs replaces wholesale. The winning layer is the
|
||||
// highest-precedence layer that sets `inputs` (project here, since it
|
||||
// wins over user).
|
||||
let inputs = settings
|
||||
|
|
@ -239,13 +202,31 @@ shared = "user"
|
|||
.unwrap();
|
||||
assert!(inputs.contains_key("project_only"));
|
||||
assert_eq!(
|
||||
inputs.get("shared").and_then(|v| v.as_str()),
|
||||
inputs.get("shared").and_then(|value| value.as_str()),
|
||||
Some("project")
|
||||
);
|
||||
assert!(
|
||||
!inputs.contains_key("user_only"),
|
||||
"project.inputs should replace user.inputs wholesale"
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.storage.as_ref())
|
||||
.and_then(|storage| storage.root.as_ref())
|
||||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
Some("/srv/fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.scheduler.as_ref())
|
||||
.and_then(|scheduler| scheduler.max_concurrent_runs),
|
||||
Some(7)
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.project
|
||||
|
|
@ -271,7 +252,7 @@ shared = "user"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn local_only_merges_workflow_project_user() {
|
||||
fn materialize_settings_layer_preserves_client_values_with_empty_server_layer() {
|
||||
let settings = materialize_settings_layer(
|
||||
EffectiveSettingsLayers::new(
|
||||
SettingsLayer::default(),
|
||||
|
|
@ -303,16 +284,13 @@ provider = "openai"
|
|||
"#,
|
||||
),
|
||||
),
|
||||
None,
|
||||
EffectiveSettingsMode::LocalOnly,
|
||||
Some(&SettingsLayer::default()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
match settings.run.as_ref().and_then(|run| run.goal.as_ref()) {
|
||||
Some(RunGoalLayer::Inline(value)) => {
|
||||
Some(value.as_source())
|
||||
}
|
||||
Some(RunGoalLayer::Inline(value)) => Some(value.as_source()),
|
||||
_ => None,
|
||||
}
|
||||
.as_deref(),
|
||||
|
|
@ -341,83 +319,7 @@ provider = "openai"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn cli_and_server_domains_from_fabro_toml_are_inert_under_remote_mode() {
|
||||
let server_settings = SettingsLayer {
|
||||
server: Some(ServerLayer {
|
||||
storage: Some(ServerStorageLayer {
|
||||
root: Some(InterpString::parse("/srv/fabro")),
|
||||
}),
|
||||
scheduler: Some(ServerSchedulerLayer {
|
||||
max_concurrent_runs: Some(9),
|
||||
}),
|
||||
..ServerLayer::default()
|
||||
}),
|
||||
..SettingsLayer::default()
|
||||
};
|
||||
|
||||
let project_with_server = layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run]
|
||||
goal = "project goal"
|
||||
|
||||
[server.storage]
|
||||
root = "/tmp/should-be-inert"
|
||||
"#,
|
||||
);
|
||||
|
||||
let settings = materialize_settings_layer(
|
||||
EffectiveSettingsLayers::new(
|
||||
SettingsLayer::default(),
|
||||
SettingsLayer::default(),
|
||||
project_with_server,
|
||||
SettingsLayer::default(),
|
||||
),
|
||||
Some(&server_settings),
|
||||
EffectiveSettingsMode::RemoteServer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
settings
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.storage.as_ref())
|
||||
.and_then(|storage| storage.root.as_ref())
|
||||
.map(InterpString::as_source)
|
||||
.as_deref(),
|
||||
Some("/srv/fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
match settings.run.as_ref().and_then(|run| run.goal.as_ref()) {
|
||||
Some(RunGoalLayer::Inline(value)) => {
|
||||
Some(value.as_source())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.as_deref(),
|
||||
Some("project goal")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.provider.as_deref()),
|
||||
Some("local")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_daemon_mode_only_applies_server_owned_overrides() {
|
||||
fn materialize_settings_layer_applies_server_owned_overrides() {
|
||||
let server_settings = SettingsLayer {
|
||||
server: Some(ServerLayer {
|
||||
storage: Some(ServerStorageLayer {
|
||||
|
|
@ -431,12 +333,9 @@ root = "/tmp/should-be-inert"
|
|||
..SettingsLayer::default()
|
||||
};
|
||||
|
||||
let settings = materialize_settings_layer(
|
||||
EffectiveSettingsLayers::default(),
|
||||
Some(&server_settings),
|
||||
EffectiveSettingsMode::LocalDaemon,
|
||||
)
|
||||
.unwrap();
|
||||
let settings =
|
||||
materialize_settings_layer(EffectiveSettingsLayers::default(), Some(&server_settings))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
settings
|
||||
|
|
|
|||
|
|
@ -2,9 +2,14 @@
|
|||
clippy::disallowed_methods,
|
||||
reason = "sync config loading utilities used at startup; not on a Tokio path"
|
||||
)]
|
||||
//! Settings resolution entrypoints are owner-first context types:
|
||||
//! [`ServerSettings`] for current server/runtime config and [`UserSettings`]
|
||||
//! for current CLI/user config. Stored `SettingsLayer` artifacts still use the
|
||||
//! per-namespace `resolve_*_from_file` helpers.
|
||||
|
||||
extern crate self as fabro_config;
|
||||
|
||||
pub mod context;
|
||||
mod defaults;
|
||||
|
||||
pub mod bind;
|
||||
|
|
@ -24,9 +29,9 @@ pub mod user;
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
pub use context::{ServerSettings, UserSettings};
|
||||
pub use defaults::{apply_builtin_defaults, defaults_layer};
|
||||
pub use error::{Error, Result};
|
||||
use fabro_types::settings::{Settings, SettingsLayer};
|
||||
pub use fabro_util::path::expand_tilde;
|
||||
pub use home::Home;
|
||||
pub use load::{
|
||||
|
|
@ -34,23 +39,14 @@ pub use load::{
|
|||
};
|
||||
pub use parse::{ParseError, parse_settings_layer};
|
||||
pub use resolve::{
|
||||
ResolveError, dev_token_auth_enabled, resolve, resolve_cli, resolve_cli_from_file,
|
||||
resolve_features, resolve_features_from_file, resolve_project, resolve_project_from_file,
|
||||
resolve_run, resolve_run_from_file, resolve_server, resolve_server_from_file,
|
||||
resolve_storage_root, resolve_workflow, resolve_workflow_from_file,
|
||||
ResolveError, dev_token_auth_enabled, resolve_cli, resolve_cli_from_file, resolve_features,
|
||||
resolve_features_from_file, resolve_project, resolve_project_from_file, resolve_run,
|
||||
resolve_run_from_file, resolve_server, resolve_server_from_file, resolve_storage_root,
|
||||
resolve_workflow, resolve_workflow_from_file,
|
||||
};
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use storage::{RunScratch, RuntimeDirectory, Storage};
|
||||
|
||||
pub fn load_and_resolve(
|
||||
layers: effective_settings::EffectiveSettingsLayers,
|
||||
server_settings: Option<&SettingsLayer>,
|
||||
mode: effective_settings::EffectiveSettingsMode,
|
||||
) -> Result<Settings> {
|
||||
let layer = effective_settings::materialize_settings_layer(layers, server_settings, mode)?;
|
||||
resolve(&layer).map_err(|errors| Error::resolve("failed to resolve settings", errors))
|
||||
}
|
||||
|
||||
/// Load a TOML config from an explicit path or `~/.fabro/{filename}`.
|
||||
///
|
||||
/// Returns `T::default()` when no explicit path is given and the default file
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
use fabro_types::settings::cli::{
|
||||
CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings,
|
||||
CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer,
|
||||
CliLayer, CliLoggingSettings, CliNamespace, CliOutputSettings, CliTargetLayer,
|
||||
CliTargetSettings, CliUpdatesSettings,
|
||||
};
|
||||
|
||||
use super::{ResolveError, require_interp};
|
||||
|
||||
pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec<ResolveError>) -> CliSettings {
|
||||
CliSettings {
|
||||
pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec<ResolveError>) -> CliNamespace {
|
||||
CliNamespace {
|
||||
target: resolve_target(layer.target.as_ref(), errors),
|
||||
auth: CliAuthSettings {
|
||||
strategy: layer.auth.as_ref().and_then(|auth| auth.strategy),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use fabro_types::settings::features::{FeaturesLayer, FeaturesSettings};
|
||||
use fabro_types::settings::features::{FeaturesLayer, FeaturesNamespace};
|
||||
|
||||
use super::ResolveError;
|
||||
|
||||
pub fn resolve_features(
|
||||
layer: &FeaturesLayer,
|
||||
_errors: &mut Vec<ResolveError>,
|
||||
) -> FeaturesSettings {
|
||||
FeaturesSettings {
|
||||
) -> FeaturesNamespace {
|
||||
FeaturesNamespace {
|
||||
session_sandboxes: layer
|
||||
.session_sandboxes
|
||||
.expect("defaults.toml should provide features.session_sandboxes"),
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ mod workflow;
|
|||
pub use cli::resolve_cli;
|
||||
pub use error::ResolveError;
|
||||
use fabro_types::settings::{
|
||||
CliSettings, FeaturesSettings, InterpString, ProjectSettings, RunSettings, ServerSettings,
|
||||
Settings, SettingsLayer, WorkflowSettings,
|
||||
CliNamespace, FeaturesNamespace, InterpString, ProjectNamespace, RunNamespace, ServerNamespace,
|
||||
SettingsLayer, WorkflowNamespace,
|
||||
};
|
||||
pub use features::resolve_features;
|
||||
pub use project::resolve_project;
|
||||
|
|
@ -20,33 +20,7 @@ pub use workflow::resolve_workflow;
|
|||
|
||||
use crate::apply_builtin_defaults;
|
||||
|
||||
pub fn resolve(file: &SettingsLayer) -> Result<Settings, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let project_layer = file.project.clone().unwrap_or_default();
|
||||
let workflow_layer = file.workflow.clone().unwrap_or_default();
|
||||
let run_layer = file.run.clone().unwrap_or_default();
|
||||
let cli_layer = file.cli.clone().unwrap_or_default();
|
||||
let server_layer = file.server.clone().unwrap_or_default();
|
||||
let features_layer = file.features.clone().unwrap_or_default();
|
||||
|
||||
let settings = Settings {
|
||||
project: resolve_project(&project_layer, &mut errors),
|
||||
workflow: resolve_workflow(&workflow_layer, &mut errors),
|
||||
run: resolve_run(&run_layer, &mut errors),
|
||||
cli: resolve_cli(&cli_layer, &mut errors),
|
||||
server: resolve_server(&server_layer, &mut errors),
|
||||
features: resolve_features(&features_layer, &mut errors),
|
||||
};
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(settings)
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result<CliSettings, Vec<ResolveError>> {
|
||||
pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result<CliNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let cli_layer = file.cli.clone().unwrap_or_default();
|
||||
|
|
@ -58,7 +32,9 @@ pub fn resolve_cli_from_file(file: &SettingsLayer) -> Result<CliSettings, Vec<Re
|
|||
}
|
||||
}
|
||||
|
||||
pub fn resolve_server_from_file(file: &SettingsLayer) -> Result<ServerSettings, Vec<ResolveError>> {
|
||||
pub fn resolve_server_from_file(
|
||||
file: &SettingsLayer,
|
||||
) -> Result<ServerNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let server_layer = file.server.clone().unwrap_or_default();
|
||||
|
|
@ -72,7 +48,7 @@ pub fn resolve_server_from_file(file: &SettingsLayer) -> Result<ServerSettings,
|
|||
|
||||
pub fn resolve_project_from_file(
|
||||
file: &SettingsLayer,
|
||||
) -> Result<ProjectSettings, Vec<ResolveError>> {
|
||||
) -> Result<ProjectNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let project_layer = file.project.clone().unwrap_or_default();
|
||||
|
|
@ -86,7 +62,7 @@ pub fn resolve_project_from_file(
|
|||
|
||||
pub fn resolve_features_from_file(
|
||||
file: &SettingsLayer,
|
||||
) -> Result<FeaturesSettings, Vec<ResolveError>> {
|
||||
) -> Result<FeaturesNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let features_layer = file.features.clone().unwrap_or_default();
|
||||
|
|
@ -98,7 +74,7 @@ pub fn resolve_features_from_file(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn resolve_run_from_file(file: &SettingsLayer) -> Result<RunSettings, Vec<ResolveError>> {
|
||||
pub fn resolve_run_from_file(file: &SettingsLayer) -> Result<RunNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let run_layer = file.run.clone().unwrap_or_default();
|
||||
|
|
@ -112,7 +88,7 @@ pub fn resolve_run_from_file(file: &SettingsLayer) -> Result<RunSettings, Vec<Re
|
|||
|
||||
pub fn resolve_workflow_from_file(
|
||||
file: &SettingsLayer,
|
||||
) -> Result<WorkflowSettings, Vec<ResolveError>> {
|
||||
) -> Result<WorkflowNamespace, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let workflow_layer = file.workflow.clone().unwrap_or_default();
|
||||
|
|
@ -165,7 +141,7 @@ mod tests {
|
|||
|
||||
use fabro_types::settings::run::{HookType, McpTransport, TlsMode};
|
||||
|
||||
use super::resolve;
|
||||
use super::resolve_run_from_file;
|
||||
use crate::parse_settings_layer;
|
||||
|
||||
#[test]
|
||||
|
|
@ -210,8 +186,8 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}"
|
|||
)
|
||||
.expect("settings fixture should parse");
|
||||
|
||||
let resolved = resolve(&settings).expect("settings should resolve");
|
||||
let mcps = &resolved.run.agent.mcps;
|
||||
let resolved = resolve_run_from_file(&settings).expect("run settings should resolve");
|
||||
let mcps = &resolved.agent.mcps;
|
||||
|
||||
assert_eq!(
|
||||
mcps.get("stdio").map(|mcp| &mcp.transport),
|
||||
|
|
@ -246,7 +222,6 @@ Authorization = "Bearer {{ env.HOOK_TOKEN }}"
|
|||
);
|
||||
|
||||
let hook = resolved
|
||||
.run
|
||||
.hooks
|
||||
.iter()
|
||||
.find(|hook| hook.name.as_deref() == Some("notify"))
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use fabro_types::settings::project::{ProjectLayer, ProjectSettings};
|
||||
use fabro_types::settings::project::{ProjectLayer, ProjectNamespace};
|
||||
|
||||
use super::ResolveError;
|
||||
|
||||
pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) -> ProjectSettings {
|
||||
ProjectSettings {
|
||||
pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) -> ProjectNamespace {
|
||||
ProjectNamespace {
|
||||
name: layer.name.clone(),
|
||||
description: layer.description.clone(),
|
||||
directory: layer
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ use fabro_types::settings::run::{
|
|||
NotificationRouteLayer, NotificationRouteSettings, PullRequestSettings, RunAgentLayer,
|
||||
RunAgentSettings, RunArtifactsLayer, RunCheckpointLayer, RunCheckpointSettings,
|
||||
RunExecutionLayer, RunExecutionSettings, RunGitLayer, RunGitSettings, RunGoal, RunGoalLayer,
|
||||
RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunPrepareLayer,
|
||||
RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, RunScmLayer,
|
||||
RunScmSettings, RunSettings, ScmGitHubSettings, StringOrSplice, TlsMode,
|
||||
RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunNamespace,
|
||||
RunPrepareLayer, RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings,
|
||||
RunScmLayer, RunScmSettings, ScmGitHubSettings, StringOrSplice, TlsMode,
|
||||
};
|
||||
|
||||
use super::ResolveError;
|
||||
|
||||
pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunSettings {
|
||||
RunSettings {
|
||||
pub fn resolve_run(layer: &RunLayer, errors: &mut Vec<ResolveError>) -> RunNamespace {
|
||||
RunNamespace {
|
||||
goal: resolve_goal(layer.goal.as_ref()),
|
||||
working_dir: layer.working_dir.clone(),
|
||||
metadata: layer.metadata.clone(),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use fabro_types::settings::server::{
|
|||
ServerAuthLayer, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsLayer,
|
||||
ServerIntegrationsSettings, ServerIpAllowlistLayer, ServerIpAllowlistOverrideLayer,
|
||||
ServerIpAllowlistOverrideSettings, ServerIpAllowlistSettings, ServerLayer, ServerListenLayer,
|
||||
ServerListenSettings, ServerLoggingSettings, ServerSchedulerSettings, ServerSettings,
|
||||
ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings,
|
||||
ServerSlateDbLayer, ServerSlateDbSettings, ServerStorageLayer, ServerStorageSettings,
|
||||
ServerWebLayer, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
|
||||
WebhookStrategy,
|
||||
|
|
@ -35,7 +35,7 @@ pub fn dev_token_auth_enabled(layer: &SettingsLayer) -> bool {
|
|||
.is_some_and(|methods| methods.contains(&ServerAuthMethod::DevToken))
|
||||
}
|
||||
|
||||
pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> ServerSettings {
|
||||
pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> ServerNamespace {
|
||||
let storage = resolve_storage(layer.storage.as_ref());
|
||||
let listen = resolve_listen(layer.listen.as_ref(), errors);
|
||||
let web = resolve_web(layer.api.as_ref(), layer.web.as_ref());
|
||||
|
|
@ -46,7 +46,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
|
|||
validate_github_webhook_ip_allowlist_for_listen(&listen, &ip_allowlist, &integrations, errors);
|
||||
validate_github_webhook_strategy(&integrations, layer.api.as_ref(), errors);
|
||||
|
||||
ServerSettings {
|
||||
ServerNamespace {
|
||||
listen,
|
||||
api: ServerApiSettings {
|
||||
url: layer.api.as_ref().and_then(|api| api.url.clone()),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use fabro_types::settings::workflow::{WorkflowLayer, WorkflowSettings};
|
||||
use fabro_types::settings::workflow::{WorkflowLayer, WorkflowNamespace};
|
||||
|
||||
use super::ResolveError;
|
||||
|
||||
pub fn resolve_workflow(
|
||||
layer: &WorkflowLayer,
|
||||
_errors: &mut Vec<ResolveError>,
|
||||
) -> WorkflowSettings {
|
||||
WorkflowSettings {
|
||||
) -> WorkflowNamespace {
|
||||
WorkflowNamespace {
|
||||
name: layer.name.clone(),
|
||||
description: layer.description.clone(),
|
||||
graph: layer
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode};
|
||||
use fabro_config::parse_settings_layer;
|
||||
use fabro_types::settings::{InterpString, SettingsLayer};
|
||||
|
||||
|
|
@ -8,8 +7,8 @@ fn parse(source: &str) -> SettingsLayer {
|
|||
|
||||
#[test]
|
||||
fn resolves_root_settings_require_explicit_server_auth_methods() {
|
||||
let errors =
|
||||
fabro_config::resolve(&SettingsLayer::default()).expect_err("empty settings should fail");
|
||||
let errors = fabro_config::resolve_server_from_file(&SettingsLayer::default())
|
||||
.expect_err("empty server settings should fail");
|
||||
|
||||
assert!(errors.iter().any(|error| {
|
||||
matches!(
|
||||
|
|
@ -40,12 +39,20 @@ provider = "not-a-provider"
|
|||
"#,
|
||||
);
|
||||
|
||||
let errors = fabro_config::resolve(&settings).expect_err("invalid shape should fail");
|
||||
let rendered = errors
|
||||
.into_iter()
|
||||
.map(|error| error.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let mut rendered = Vec::new();
|
||||
rendered.extend(
|
||||
fabro_config::resolve_server_from_file(&settings)
|
||||
.expect_err("invalid server settings should fail")
|
||||
.into_iter()
|
||||
.map(|error| error.to_string()),
|
||||
);
|
||||
rendered.extend(
|
||||
fabro_config::resolve_run_from_file(&settings)
|
||||
.expect_err("invalid run settings should fail")
|
||||
.into_iter()
|
||||
.map(|error| error.to_string()),
|
||||
);
|
||||
let rendered = rendered.join("\n");
|
||||
|
||||
assert!(rendered.contains("server.listen.address"));
|
||||
assert!(rendered.contains("server.auth.github.allowed_usernames"));
|
||||
|
|
@ -53,66 +60,45 @@ provider = "not-a-provider"
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn load_and_resolve_merges_layers_before_resolution() {
|
||||
let settings = fabro_config::load_and_resolve(
|
||||
EffectiveSettingsLayers::new(
|
||||
SettingsLayer::default(),
|
||||
parse(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[workflow]
|
||||
graph = "graphs/workflow.dot"
|
||||
"#,
|
||||
),
|
||||
parse(
|
||||
r#"
|
||||
fn namespace_resolvers_cover_root_level_settings_shape() {
|
||||
let settings = parse(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[project]
|
||||
directory = ".fabro"
|
||||
"#,
|
||||
),
|
||||
parse(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[workflow]
|
||||
graph = "graphs/workflow.dot"
|
||||
|
||||
[server.storage]
|
||||
root = "/srv/fabro"
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[run.model]
|
||||
provider = "openai"
|
||||
name = "gpt-5"
|
||||
"#,
|
||||
),
|
||||
),
|
||||
None,
|
||||
EffectiveSettingsMode::LocalOnly,
|
||||
)
|
||||
.expect("layers should load and resolve");
|
||||
);
|
||||
|
||||
assert_eq!(settings.project.directory, ".fabro");
|
||||
assert_eq!(settings.workflow.graph, "graphs/workflow.dot");
|
||||
assert_eq!(settings.server.storage.root.as_source(), "/srv/fabro");
|
||||
let project = fabro_config::resolve_project_from_file(&settings)
|
||||
.expect("project settings should resolve");
|
||||
let workflow = fabro_config::resolve_workflow_from_file(&settings)
|
||||
.expect("workflow settings should resolve");
|
||||
let server =
|
||||
fabro_config::resolve_server_from_file(&settings).expect("server settings should resolve");
|
||||
let run = fabro_config::resolve_run_from_file(&settings).expect("run settings should resolve");
|
||||
|
||||
assert_eq!(project.directory, ".fabro");
|
||||
assert_eq!(workflow.graph, "graphs/workflow.dot");
|
||||
assert_eq!(server.storage.root.as_source(), "/srv/fabro");
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.model
|
||||
.provider
|
||||
.as_ref()
|
||||
.map(InterpString::as_source),
|
||||
run.model.provider.as_ref().map(InterpString::as_source),
|
||||
Some("openai".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.model
|
||||
.name
|
||||
.as_ref()
|
||||
.map(InterpString::as_source),
|
||||
run.model.name.as_ref().map(InterpString::as_source),
|
||||
Some("gpt-5".to_string())
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -225,7 +225,6 @@ mod tests {
|
|||
server::create_test_app_state_with_session_key(
|
||||
SettingsLayer::default(),
|
||||
Some(SESSION_SECRET),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ use crate::error::ApiError;
|
|||
use crate::jwt_auth::AuthenticatedService;
|
||||
use crate::run_selector::{ResolveRunError, resolve_run_by_selector};
|
||||
use crate::server::{AppState, PaginationParams};
|
||||
use crate::settings_view;
|
||||
|
||||
fn paginated_response<T: serde::Serialize>(
|
||||
items: Vec<T>,
|
||||
|
|
@ -602,22 +601,8 @@ pub(crate) async fn list_query_history(
|
|||
pub(crate) async fn get_server_settings(
|
||||
_auth: AuthenticatedService,
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Query(query): Query<settings_view::SettingsQuery>,
|
||||
) -> Response {
|
||||
match query.view {
|
||||
settings_view::SettingsApiView::Layer => {
|
||||
(StatusCode::OK, Json(settings::server_settings())).into_response()
|
||||
}
|
||||
settings_view::SettingsApiView::Resolved => {
|
||||
let mut response =
|
||||
(StatusCode::OK, Json(settings::resolved_server_settings())).into_response();
|
||||
response.headers_mut().insert(
|
||||
settings_view::RESOLVED_VIEW_HEADER_NAME,
|
||||
axum::http::HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE),
|
||||
);
|
||||
response
|
||||
}
|
||||
}
|
||||
(StatusCode::OK, Json(settings::server_settings())).into_response()
|
||||
}
|
||||
|
||||
// ── System ────────────────────────────────────────────────────────────
|
||||
|
|
@ -1529,208 +1514,50 @@ mod insights {
|
|||
|
||||
mod settings {
|
||||
pub(super) fn server_settings() -> serde_json::Value {
|
||||
// v2 SettingsLayer shape — matches what /api/v1/settings returns in
|
||||
// production, so the demo renders identically.
|
||||
serde_json::json!({
|
||||
"_version": 1,
|
||||
"server": {
|
||||
"storage": {
|
||||
"root": "/home/fabro/.fabro"
|
||||
},
|
||||
"scheduler": {
|
||||
"max_concurrent_runs": 10
|
||||
},
|
||||
"api": {
|
||||
"url": "https://api.fabro.example.com"
|
||||
},
|
||||
"web": {
|
||||
"enabled": true,
|
||||
"url": "https://fabro.example.com"
|
||||
},
|
||||
"auth": {
|
||||
"api": {
|
||||
"jwt": { "enabled": true }
|
||||
},
|
||||
"web": {
|
||||
"allowed_usernames": ["brynary", "alice"],
|
||||
"providers": {
|
||||
"github": {
|
||||
"enabled": true,
|
||||
"client_id": "Iv1.abc123"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"integrations": {
|
||||
"github": {
|
||||
"app_id": "12345",
|
||||
"client_id": "Iv1.abc123",
|
||||
"slug": "fabro-dev"
|
||||
}
|
||||
}
|
||||
},
|
||||
"run": {
|
||||
"model": {
|
||||
"provider": "anthropic",
|
||||
"name": "claude-sonnet"
|
||||
},
|
||||
"sandbox": {
|
||||
"provider": "daytona",
|
||||
"daytona": {
|
||||
"auto_stop_interval": 60,
|
||||
"network": "block"
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"session_sandboxes": false,
|
||||
"retros": false
|
||||
}
|
||||
})
|
||||
}
|
||||
let settings = fabro_config::parse_settings_layer(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
pub(super) fn resolved_server_settings() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"project": {
|
||||
"directory": "."
|
||||
},
|
||||
"workflow": {
|
||||
"graph": "workflow.fabro"
|
||||
},
|
||||
"run": {
|
||||
"model": {
|
||||
"provider": "anthropic",
|
||||
"name": "claude-sonnet",
|
||||
"fallbacks": []
|
||||
},
|
||||
"execution": {
|
||||
"mode": "normal",
|
||||
"approval": "prompt",
|
||||
"retros": true
|
||||
},
|
||||
"sandbox": {
|
||||
"provider": "daytona",
|
||||
"preserve": false,
|
||||
"devcontainer": false,
|
||||
"env": {},
|
||||
"local": {
|
||||
"worktree_mode": "clean"
|
||||
},
|
||||
"daytona": {
|
||||
"auto_stop_interval": 60,
|
||||
"labels": {},
|
||||
"network": "block",
|
||||
"skip_clone": false
|
||||
}
|
||||
},
|
||||
"notifications": {},
|
||||
"interviews": {},
|
||||
"agent": {
|
||||
"mcps": {}
|
||||
},
|
||||
"hooks": [],
|
||||
"scm": {},
|
||||
"artifacts": {
|
||||
"include": []
|
||||
},
|
||||
"inputs": {},
|
||||
"metadata": {},
|
||||
"git": {},
|
||||
"prepare": {
|
||||
"commands": [],
|
||||
"timeout_ms": 300000
|
||||
},
|
||||
"checkpoint": {
|
||||
"exclude_globs": []
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"auth": {},
|
||||
"exec": {
|
||||
"prevent_idle_sleep": false,
|
||||
"model": {},
|
||||
"agent": {
|
||||
"mcps": {}
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"format": "text",
|
||||
"verbosity": "normal"
|
||||
},
|
||||
"updates": {
|
||||
"check": true
|
||||
},
|
||||
"logging": {}
|
||||
},
|
||||
"server": {
|
||||
"api": {
|
||||
"url": "https://api.fabro.example.com"
|
||||
},
|
||||
"web": {
|
||||
"enabled": true,
|
||||
"url": "https://fabro.example.com"
|
||||
},
|
||||
"auth": {
|
||||
"api": {
|
||||
"jwt": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"allowed_usernames": ["brynary", "alice"],
|
||||
"providers": {
|
||||
"github": {
|
||||
"enabled": true,
|
||||
"client_id": "Iv1.abc123"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"root": "/home/fabro/.fabro"
|
||||
},
|
||||
"artifacts": {
|
||||
"prefix": "",
|
||||
"store": {
|
||||
"type": "local",
|
||||
"root": ""
|
||||
}
|
||||
},
|
||||
"slatedb": {
|
||||
"prefix": "",
|
||||
"store": {
|
||||
"type": "local",
|
||||
"root": ""
|
||||
},
|
||||
"flush_interval": "0s"
|
||||
},
|
||||
"scheduler": {
|
||||
"max_concurrent_runs": 10
|
||||
},
|
||||
"logging": {},
|
||||
"integrations": {
|
||||
"github": {
|
||||
"enabled": false,
|
||||
"strategy": "token",
|
||||
"app_id": "12345",
|
||||
"client_id": "Iv1.abc123",
|
||||
"slug": "fabro-dev",
|
||||
"permissions": {}
|
||||
},
|
||||
"slack": {
|
||||
"enabled": false
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false
|
||||
},
|
||||
"teams": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"session_sandboxes": false
|
||||
}
|
||||
})
|
||||
[server.listen]
|
||||
type = "tcp"
|
||||
address = "127.0.0.1:32276"
|
||||
|
||||
[server.api]
|
||||
url = "https://api.fabro.example.com"
|
||||
|
||||
[server.web]
|
||||
enabled = true
|
||||
url = "https://fabro.example.com"
|
||||
|
||||
[server.auth]
|
||||
methods = ["github"]
|
||||
|
||||
[server.auth.github]
|
||||
allowed_usernames = ["brynary", "alice"]
|
||||
|
||||
[server.storage]
|
||||
root = "/home/fabro/.fabro"
|
||||
|
||||
[server.scheduler]
|
||||
max_concurrent_runs = 10
|
||||
|
||||
[server.integrations.github]
|
||||
enabled = true
|
||||
strategy = "app"
|
||||
app_id = "12345"
|
||||
client_id = "Iv1.abc123"
|
||||
slug = "fabro-dev"
|
||||
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
"#,
|
||||
)
|
||||
.expect("demo settings fixture should parse");
|
||||
|
||||
serde_json::to_value(
|
||||
fabro_config::ServerSettings::from_layer(&settings)
|
||||
.expect("demo settings fixture should resolve"),
|
||||
)
|
||||
.expect("demo settings should serialize")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue