mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-19 00:03:30 +00:00
refactor(config): unify machine config as settings.toml
Consolidate CLI and server machine defaults under settings.toml, including loader renames, writer preservation fixes, same-machine manifest handling, and docs/test updates for the new config model.
This commit is contained in:
parent
c66ba89875
commit
36ff3c5377
74 changed files with 811 additions and 381 deletions
|
|
@ -20,7 +20,7 @@ services:
|
|||
environment:
|
||||
- FABRO_DEMO=1
|
||||
volumes:
|
||||
- ./demo-server.toml:/root/.fabro/server.toml:ro
|
||||
- ./demo-settings.toml:/root/.fabro/settings.toml:ro
|
||||
- ../apps/fabro-web/public:/app/apps/fabro-web/public
|
||||
depends_on:
|
||||
- api
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Common flags:
|
|||
| `--sandbox` | — | Override default sandbox provider |
|
||||
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
|
||||
|
||||
See [Server Configuration](/administration/server-configuration) for the full `server.toml` reference.
|
||||
See [Server Configuration](/administration/server-configuration) for the full `settings.toml` reference.
|
||||
|
||||
## Submitting runs
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ In the server interface, human-in-the-loop questions are served over HTTP instea
|
|||
|
||||
## Authentication
|
||||
|
||||
The server supports two authentication strategies, configurable in `server.toml`:
|
||||
The server supports two authentication strategies, configurable in `settings.toml`:
|
||||
|
||||
- **JWT** — EdDSA-signed bearer tokens. Used by the web UI. See [API Overview](/api-reference/overview#jwt-bearer-token) for token format.
|
||||
- **mTLS** — Mutual TLS with client certificates. Used for service-to-service communication. See [API Overview](/api-reference/overview#mtls-mutual-tls) for setup.
|
||||
|
|
@ -108,9 +108,9 @@ Send the `X-Fabro-Demo: 1` header on any API request to get static mock data wit
|
|||
|
||||
## Pointing the CLI at a server
|
||||
|
||||
The CLI can target a running Fabro server for commands that support a remote API. Configure `~/.fabro/user.toml`:
|
||||
The CLI can target a running Fabro server for commands that support a remote API. Configure `~/.fabro/settings.toml`:
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[server]
|
||||
target = "https://fabro.example.com:3000/api/v1"
|
||||
```
|
||||
|
|
@ -129,7 +129,7 @@ See [User Configuration](/reference/user-configuration#server-section) for the f
|
|||
|
||||
<Columns cols={2}>
|
||||
<Card title="Server Configuration" icon="gear" href="/administration/server-configuration">
|
||||
Full server.toml reference — authentication, TLS, run defaults, and more.
|
||||
Full settings.toml reference — authentication, TLS, run defaults, and more.
|
||||
</Card>
|
||||
<Card title="Deploy to Railway" icon="train" href="/administration/deploy-railway">
|
||||
Step-by-step guide for deploying Fabro on Railway.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ Fabro supports three sandbox providers: `local` (no isolation), `docker` (contai
|
|||
|
||||
For cloud sandboxes (Daytona), you can control outbound network access with the `network` field in `[sandbox.daytona]`. Three modes are available: `"allow_all"` (default), `"block"`, and `{ allow_list = ["..."] }` for CIDR-based egress filtering.
|
||||
|
||||
Server defaults in `server.toml` apply when a run config doesn't specify `network`. Individual run configs can override the server default.
|
||||
Server defaults in `settings.toml` apply when a run config doesn't specify `network`. Individual run configs can override the server default.
|
||||
|
||||
See [Environments — Network access](/execution/environments#network-access) for syntax examples and the full reference.
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ Fabro is single-tenant software designed for small, trusted teams. The following
|
|||
### Authentication
|
||||
|
||||
- **Enable authentication.** Fabro supports GitHub OAuth and Tailscale header-based auth for the web app. Do not use `insecure_disabled` outside of local development.
|
||||
- **Configure a username allowlist.** Both GitHub and Tailscale auth support `allowed_usernames` in `server.toml`. An empty allowlist rejects all requests.
|
||||
- **Configure a username allowlist.** Both GitHub and Tailscale auth support `allowed_usernames` in `settings.toml`. An empty allowlist rejects all requests.
|
||||
- **Use JWT to connect the web app to the API.** Configure `FABRO_JWT_PRIVATE_KEY` on the web app and `FABRO_JWT_PUBLIC_KEY` on the API server. JWT tokens are Ed25519-signed and short-lived (30 seconds).
|
||||
- **Use mTLS for machine-to-machine API access.** Configure `[api.tls]` in `server.toml` with server cert, key, and CA. Set client auth to `Required` for programmatic clients (CI, scripts).
|
||||
- **Use mTLS for machine-to-machine API access.** Configure `[api.tls]` in `settings.toml` with server cert, key, and CA. Set client auth to `Required` for programmatic clients (CI, scripts).
|
||||
|
||||
### Secrets
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
---
|
||||
title: "Server Configuration"
|
||||
description: "Server config file, CLI overrides, and environment variables"
|
||||
description: "Server-owned settings.toml sections, CLI overrides, and environment variables"
|
||||
---
|
||||
|
||||
## Config file
|
||||
|
||||
The server config file at `~/.fabro/server.toml` controls how `fabro server start` behaves — API binding, authentication, run defaults, and more. The [Quick Start](/getting-started/quick-start) doesn't require one, but production deployments should configure it explicitly.
|
||||
`fabro server start` reads `~/.fabro/settings.toml` by default. This is the same file schema used by the CLI.
|
||||
|
||||
This file configures the server process itself. It is separate from `[server].target` in `~/.fabro/user.toml`, which tells CLI commands how to connect to a server.
|
||||
On a same-machine setup, the CLI and server share one `settings.toml`. On a remote deployment, the server machine has its own `settings.toml`, and the client machine keeps a separate local `settings.toml` for CLI-only values such as `[server].target`.
|
||||
|
||||
<Note>
|
||||
Legacy `server.toml`, `user.toml`, and `cli.toml` are ignored with a warning. Rename them to `settings.toml`.
|
||||
</Note>
|
||||
|
||||
### Which sections are server-owned
|
||||
|
||||
| Scope | Examples |
|
||||
|---|---|
|
||||
| Server-owned | `storage_dir`, `max_concurrent_runs`, `[web]`, `[api]`, `[features]` |
|
||||
| Shared with CLI and workflow defaults | `[llm]`, `[log]`, `[git]`, `[setup]`, `[sandbox]`, `[checkpoint]`, `[vars]`, `[pull_request]` |
|
||||
|
||||
The CLI-only `[server]` section still belongs in the client machine's `settings.toml`. It tells CLI commands where to find a server. The server process does not read `[server].target` for its own binding or routing.
|
||||
|
||||
### Full reference
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
# Maximum concurrent workflow runs (default: 5)
|
||||
max_concurrent_runs = 8
|
||||
|
||||
|
|
@ -52,7 +65,7 @@ email = "fabro-bot@company.com"
|
|||
[git.webhooks]
|
||||
strategy = "tailscale_funnel"
|
||||
|
||||
# Run defaults — applied to every run unless overridden by the run config
|
||||
# Run defaults — applied to every run unless overridden by workflow/project config
|
||||
[llm]
|
||||
model = "claude-sonnet-4-5"
|
||||
provider = "anthropic"
|
||||
|
|
@ -85,7 +98,7 @@ default_branch = "main"
|
|||
|
||||
### CLI overrides
|
||||
|
||||
Several `server.toml` settings can be overridden via `fabro server start` flags:
|
||||
Several `settings.toml` settings can be overridden via `fabro server start` flags:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
|
|
@ -95,14 +108,20 @@ Several `server.toml` settings can be overridden via `fabro server start` flags:
|
|||
| `--provider` | — | Override default LLM provider |
|
||||
| `--sandbox` | — | Override default sandbox provider |
|
||||
| `--max-concurrent-runs` | `5` | Maximum concurrent run executions |
|
||||
| `--config` | `~/.fabro/server.toml` | Path to server config file |
|
||||
| `--config` | `~/.fabro/settings.toml` | Path to server config file |
|
||||
| `--dry-run` | — | Execute with simulated LLM backend |
|
||||
|
||||
CLI flags take precedence over `server.toml` values. See [Run Configuration — Precedence](/execution/run-configuration#precedence) for the full resolution order.
|
||||
CLI flags take precedence over `settings.toml` values. See [Run Configuration — Precedence](/execution/run-configuration#precedence) for the full resolution order.
|
||||
|
||||
### Run defaults
|
||||
|
||||
The `[llm]`, `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` sections in `server.toml` act as defaults for every run. A run config TOML can override any of these. For `[vars]`, Daytona labels, and checkpoint exclude globs, values are **merged** — the run config wins on key collisions. All other fields use "first non-empty wins" precedence.
|
||||
The `[llm]`, `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` sections in `settings.toml` act as defaults for every run.
|
||||
|
||||
On a same-machine setup, `settings.toml` is the shared machine-default layer under `workflow.toml` / `run.toml` and `fabro.toml`.
|
||||
|
||||
On a remote setup, the client bundles workflow, project, and user config into the run manifest. The server then layers those bundled client configs over its own local defaults for run-shaped fields. Server-owned values like `storage_dir`, `[api]`, `[web]`, `[features]`, and `max_concurrent_runs` always come from the server machine's own `settings.toml` or `fabro server start` flags.
|
||||
|
||||
For `[vars]`, Daytona labels, and checkpoint exclude globs, values are **merged** and the more specific layer wins on key collisions. All other fields use "first non-empty wins" precedence.
|
||||
|
||||
### `[log]` section
|
||||
|
||||
|
|
@ -121,7 +140,7 @@ Customize the git author identity used for checkpoint commits. When not set, def
|
|||
| `name` | Git author name | `"fabro"` |
|
||||
| `email` | Git author email | `"fabro@local"` |
|
||||
|
||||
The CLI can also set `[git.author]` in `user.toml` to override the server default.
|
||||
On same-machine setups, the CLI and server read the same `[git.author]`. On remote setups, the server uses its local `settings.toml`.
|
||||
|
||||
### `[git.webhooks]` section
|
||||
|
||||
|
|
@ -139,9 +158,9 @@ Configure checkpoint behavior for all runs.
|
|||
|
||||
| Key | Description |
|
||||
|---|---|
|
||||
| `exclude_globs` | Glob patterns for files to exclude from checkpoint commits (e.g. `["**/node_modules/**"]`) |
|
||||
| `exclude_globs` | Glob patterns for files to exclude from checkpoint commits (for example, `["**/node_modules/**"]`) |
|
||||
|
||||
Exclude globs from `server.toml` and run configs are merged (union, deduplicated). See [Run Configuration — Checkpoint](/execution/run-configuration#checkpoint) for per-run configuration.
|
||||
Exclude globs from `settings.toml` and run configs are merged (union, deduplicated). See [Run Configuration — Checkpoint](/execution/run-configuration#checkpoint) for per-run configuration.
|
||||
|
||||
### `[features]` section
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ Hooks are defined as `[[hooks]]` entries in any of these TOML config files:
|
|||
|
||||
- **`fabro.toml`** — project-level hooks, apply to all workflows in the project
|
||||
- **`workflow.toml`** — per-workflow hooks
|
||||
- **`~/.fabro/user.toml`** or **`~/.fabro/server.toml`** — global defaults for all runs
|
||||
- **`~/.fabro/settings.toml`** or **`~/.fabro/settings.toml`** — global defaults for all runs
|
||||
|
||||
See [Merging hook configs](#merging-hook-configs) for how these layers combine.
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ Command hooks do **not** fail open. A non-zero exit code (other than 0 or 2) pro
|
|||
|
||||
Hooks from multiple config files are merged in this order (later layers win on name collisions):
|
||||
|
||||
1. **`~/.fabro/user.toml`** or **`~/.fabro/server.toml`** — global defaults
|
||||
1. **`~/.fabro/settings.toml`** or **`~/.fabro/settings.toml`** — global defaults
|
||||
2. **`fabro.toml`** — project-level overrides
|
||||
3. **`workflow.toml`** — per-workflow overrides
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ For example, a server named `filesystem` exposing a `read_file` tool becomes `mc
|
|||
|
||||
MCP servers can be configured in two places:
|
||||
|
||||
- **`~/.fabro/user.toml`** — applies to `fabro exec` sessions. See [User Configuration](/reference/user-configuration#mcp_servers-section).
|
||||
- **`~/.fabro/settings.toml`** — applies to `fabro exec` sessions. See [User Configuration](/reference/user-configuration#mcp_servers-section).
|
||||
- **Run config TOML** — applies to workflow runs (`fabro run`). See [Run Configuration](/execution/run-configuration#mcp_servers).
|
||||
|
||||
Each server entry specifies a transport type and optional timeouts. The server name is the TOML table key and is used in qualified tool names.
|
||||
|
|
|
|||
|
|
@ -17,18 +17,18 @@ The versioned API is served by `fabro server start`, which defaults to:
|
|||
http://localhost:3000/api/v1
|
||||
```
|
||||
|
||||
The base URL is configurable via `server.toml`:
|
||||
The base URL is configurable via `settings.toml`:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[api]
|
||||
base_url = "https://fabro.example.com/api/v1"
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
The API supports two authentication strategies, configured in `server.toml`:
|
||||
The API supports two authentication strategies, configured in `settings.toml`:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[api]
|
||||
authentication_strategies = ["jwt"]
|
||||
```
|
||||
|
|
@ -58,7 +58,7 @@ Set the verification key via the `FABRO_JWT_PUBLIC_KEY` environment variable (PE
|
|||
|
||||
With mTLS, the client authenticates using a TLS client certificate. Configure both the strategy and TLS paths:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[api]
|
||||
authentication_strategies = ["mtls"]
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ The Common Name (CN) from the client certificate identifies the user.
|
|||
|
||||
You can configure both strategies. They are tried in order — the first successful match wins:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[api]
|
||||
authentication_strategies = ["jwt", "mtls"]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ The run branch is a regular Git branch that grows one commit per completed node.
|
|||
|
||||
### Run branch commits
|
||||
|
||||
After each node finishes, Fabro stages file changes and creates a commit on the run branch. Files matching `[checkpoint] exclude_globs` patterns (configured in [run.toml](/execution/run-configuration#checkpoint) or [server.toml](/administration/server-configuration#checkpoint-section)) are excluded from staging:
|
||||
After each node finishes, Fabro stages file changes and creates a commit on the run branch. Files matching `[checkpoint] exclude_globs` patterns (configured in [run.toml](/execution/run-configuration#checkpoint) or [settings.toml](/administration/server-configuration#checkpoint-section)) are excluded from staging:
|
||||
|
||||
```
|
||||
fabro(01JKXYZ...): plan (success)
|
||||
|
|
|
|||
|
|
@ -135,9 +135,9 @@ To skip retro generation for a single run when retros are enabled, pass `--no-re
|
|||
fabro run workflow.fabro --no-retro
|
||||
```
|
||||
|
||||
Retros can also be enabled server-wide in `server.toml`:
|
||||
Retros can also be enabled server-wide in `settings.toml`:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[features]
|
||||
retros = true
|
||||
```
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ NODE_ENV = "production"
|
|||
| `"literal"` | Static value passed as-is |
|
||||
| `"${env.VARNAME}"` | Resolved from the host environment at load time. Missing vars produce a hard error. |
|
||||
|
||||
Host env references must be whole-value only — partial interpolation like `"prefix-${env.X}"` is not supported. Sandbox env vars from `server.toml` defaults and the run config are merged, with the run config winning on key collisions.
|
||||
Host env references must be whole-value only — partial interpolation like `"prefix-${env.X}"` is not supported. Sandbox env vars from `settings.toml` defaults and the run config are merged, with the run config winning on key collisions.
|
||||
|
||||
### `[checkpoint]`
|
||||
|
||||
|
|
@ -233,7 +233,7 @@ exclude_globs = ["**/node_modules/**", "**/.cache/**", "**/dist/**"]
|
|||
|---|---|
|
||||
| `exclude_globs` | Glob patterns for files to exclude from checkpoint commits. Uses git pathspec `:(glob,exclude)` syntax. |
|
||||
|
||||
Exclude globs from `server.toml` defaults and the run config are merged (union, deduplicated).
|
||||
Exclude globs from `settings.toml` defaults and the run config are merged (union, deduplicated).
|
||||
|
||||
### `[vars]`
|
||||
|
||||
|
|
@ -391,7 +391,7 @@ Settings can come from multiple sources. Fabro resolves them in this order (firs
|
|||
| Run config TOML | |
|
||||
| CLI flags (`--model`, `--provider`, `--sandbox`) | |
|
||||
| Project defaults (`fabro.toml`) | |
|
||||
| Server defaults (`~/.fabro/server.toml`) | |
|
||||
| Server defaults (`~/.fabro/settings.toml`) | |
|
||||
| Graphviz graph attributes (`default_model`, `default_provider`) | |
|
||||
| Built-in defaults | Lowest |
|
||||
|
||||
|
|
@ -423,12 +423,12 @@ Project defaults are merged with run config values using the same rules as serve
|
|||
|
||||
### Server defaults
|
||||
|
||||
When running via `fabro server start`, the server config at `~/.fabro/server.toml` can set default values for `[llm]`, `[setup]`, `[sandbox]`, and `[vars]`. These defaults are applied to every run unless the run config overrides them.
|
||||
When running via `fabro server start`, the server config at `~/.fabro/settings.toml` can set default values for `[llm]`, `[setup]`, `[sandbox]`, and `[vars]`. These defaults are applied to every run unless the run config overrides them.
|
||||
|
||||
For variables, defaults and run config are **merged** — the run config wins on key collisions:
|
||||
|
||||
```toml
|
||||
# ~/.fabro/server.toml
|
||||
# ~/.fabro/settings.toml
|
||||
[vars]
|
||||
default_key = "from_server"
|
||||
shared = "from_server"
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ auto_stop_interval = 30
|
|||
|
||||
## Server defaults
|
||||
|
||||
When running via `fabro server start`, the server config at `~/.fabro/server.toml` can set default Daytona settings for all runs. Run config TOML values override server defaults. Labels are **merged** — run config labels win on key collisions. The `network` setting uses simple override (run config replaces the server default entirely).
|
||||
When running via `fabro server start`, the server config at `~/.fabro/settings.toml` can set default Daytona settings for all runs. Run config TOML values override server defaults. Labels are **merged** — run config labels win on key collisions. The `network` setting uses simple override (run config replaces the server default entirely).
|
||||
|
||||
See [Server Configuration](/administration/server-configuration) for details.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Fabro uses a [GitHub App](https://docs.github.com/en/apps/overview) to authentic
|
|||
|
||||
4. GitHub redirects back to Fabro, which automatically:
|
||||
- Exchanges the temporary code for permanent app credentials
|
||||
- Writes `app_id`, `client_id`, and `slug` to `~/.fabro/server.toml`
|
||||
- Writes `app_id`, `client_id`, and `slug` to `~/.fabro/settings.toml`
|
||||
- Stores `GITHUB_APP_CLIENT_SECRET`, `GITHUB_APP_WEBHOOK_SECRET`, and `GITHUB_APP_PRIVATE_KEY` in the server secret store
|
||||
- Generates a `SESSION_SECRET` for web app sessions
|
||||
- Redirects you to the login page
|
||||
|
|
@ -61,8 +61,8 @@ The GitHub App check verifies five fields:
|
|||
|
||||
| Field | Source |
|
||||
|---|---|
|
||||
| `git.app_id` | `~/.fabro/server.toml` |
|
||||
| `git.client_id` | `~/.fabro/server.toml` |
|
||||
| `git.app_id` | `~/.fabro/settings.toml` |
|
||||
| `git.client_id` | `~/.fabro/settings.toml` |
|
||||
| `GITHUB_APP_CLIENT_SECRET` | Server secret store |
|
||||
| `GITHUB_APP_WEBHOOK_SECRET` | Server secret store |
|
||||
| `GITHUB_APP_PRIVATE_KEY` | Server secret store |
|
||||
|
|
@ -73,9 +73,9 @@ If all five are set, the check passes. If none are set, it warns (GitHub integra
|
|||
|
||||
The GitHub App configuration lives in two places:
|
||||
|
||||
### `~/.fabro/server.toml`
|
||||
### `~/.fabro/settings.toml`
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[git]
|
||||
provider = "github"
|
||||
app_id = "123456"
|
||||
|
|
@ -111,11 +111,11 @@ The web app uses the GitHub App's OAuth credentials to authenticate users:
|
|||
3. User authorizes the app on GitHub
|
||||
4. GitHub redirects back with an authorization code
|
||||
5. Fabro exchanges the code for an access token and fetches the user's profile and verified email
|
||||
6. Fabro checks the username against the `allowed_usernames` list in `server.toml`
|
||||
6. Fabro checks the username against the `allowed_usernames` list in `settings.toml`
|
||||
|
||||
Configure allowed users in `server.toml`:
|
||||
Configure allowed users in `settings.toml`:
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[web.auth]
|
||||
provider = "github"
|
||||
allowed_usernames = ["alice", "bob"]
|
||||
|
|
@ -191,7 +191,7 @@ The app is installed but doesn't have access to this specific repository. Update
|
|||
|
||||
### "GitHub App authentication failed"
|
||||
|
||||
The `app_id` in `server.toml` or the `GITHUB_APP_PRIVATE_KEY` environment variable is incorrect. Re-run the setup flow or verify the values match your GitHub App.
|
||||
The `app_id` in `settings.toml` or the `GITHUB_APP_PRIVATE_KEY` environment variable is incorrect. Re-run the setup flow or verify the values match your GitHub App.
|
||||
|
||||
### Clone fails for private repositories
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ FABRO_SLACK_APP_TOKEN=xapp-your-app-token
|
|||
|
||||
Optionally, set a default channel in your [server configuration](/administration/server-configuration):
|
||||
|
||||
```toml title="server.toml"
|
||||
```toml title="settings.toml"
|
||||
[slack]
|
||||
default_channel = "#fabro-reviews"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ fabro server start
|
|||
|
||||
### Configuration
|
||||
|
||||
The server reads `~/.fabro/server.toml` for default settings (model, sandbox, variables, authentication). This file is live-reloaded — changes take effect within seconds without restarting the server.
|
||||
The server reads `~/.fabro/settings.toml` for default settings (model, sandbox, variables, authentication). This file is live-reloaded — changes take effect within seconds without restarting the server.
|
||||
|
||||
Key server config options:
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ In API mode, human-in-the-loop questions are served over HTTP instead of termina
|
|||
|
||||
### Authentication
|
||||
|
||||
API mode supports two authentication strategies, configurable in `server.toml`:
|
||||
API mode supports two authentication strategies, configurable in `settings.toml`:
|
||||
|
||||
- **JWT** — EdDSA-signed tokens (used by the web UI)
|
||||
- **mTLS** — Mutual TLS with client certificates (used for service-to-service communication)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ Connection-target flags like `--storage-dir` and `--server` are command-specific
|
|||
|
||||
## Configuration
|
||||
|
||||
CLI defaults can be set in `~/.fabro/user.toml` so you don't have to pass common flags every time:
|
||||
CLI defaults can be set in `~/.fabro/settings.toml` so you don't have to pass common flags every time:
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[exec]
|
||||
provider = "anthropic"
|
||||
model = "claude-opus-4-6"
|
||||
|
|
@ -41,7 +41,7 @@ target = "https://fabro.example.com:3000/api/v1"
|
|||
|
||||
`fabro model` uses `[server].target` by default when no explicit `--storage-dir` is passed. `fabro exec` remains a local session unless you pass `--server`, even if `[server].target` is configured.
|
||||
|
||||
CLI flags always override `user.toml` values, which override hardcoded defaults.
|
||||
CLI flags always override `settings.toml` values, which override hardcoded defaults.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ fabro settings demo
|
|||
fabro settings run.toml
|
||||
```
|
||||
|
||||
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/user.toml` and the nearest `fabro.toml`.
|
||||
With no argument, Fabro prints the merged ambient defaults from `~/.fabro/settings.toml` and the nearest `fabro.toml`.
|
||||
|
||||
When you pass a workflow name or path:
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ fabro server start --sandbox daytona --max-concurrent-runs 4
|
|||
| `--dry-run` | Execute with simulated LLM backend | — |
|
||||
| `--sandbox <SANDBOX>` | Sandbox for agent tools: `local`, `docker`, or `daytona` | — |
|
||||
| `--max-concurrent-runs <N>` | Maximum number of concurrent run executions | — |
|
||||
| `--config <PATH>` | Path to server config file | `~/.fabro/server.toml` |
|
||||
| `--config <PATH>` | Path to server config file | `~/.fabro/settings.toml` |
|
||||
|
||||
Demo mode is per-request: send the `X-Fabro-Demo: 1` header to get static demo data with auth disabled.
|
||||
|
||||
|
|
@ -715,7 +715,7 @@ fabro upgrade --version 0.6.0
|
|||
| `--force` | Upgrade even if already on the target version |
|
||||
| `--dry-run` | Preview what would happen without making changes |
|
||||
|
||||
Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`user.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag.
|
||||
Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`settings.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag.
|
||||
|
||||
## `fabro artifact list`
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,49 @@
|
|||
---
|
||||
title: "User Configuration"
|
||||
description: "Configure default user settings for Fabro with user.toml"
|
||||
title: "Settings Configuration"
|
||||
description: "Configure CLI and shared machine defaults with settings.toml"
|
||||
---
|
||||
|
||||
Fabro loads user defaults from `~/.fabro/user.toml` so you don't have to pass common flags every time. The file is optional — if it doesn't exist, built-in defaults are used.
|
||||
Fabro loads machine defaults from `~/.fabro/settings.toml`. The file is optional. If it does not exist, Fabro falls back to built-in defaults.
|
||||
|
||||
On a same-machine setup, the CLI and server both read this file. On a remote setup, each machine has its own `settings.toml` and reads the sections relevant to that process.
|
||||
|
||||
<Note>
|
||||
Legacy `cli.toml`, `user.toml`, and `server.toml` are ignored with a warning. Rename them to `settings.toml`.
|
||||
</Note>
|
||||
|
||||
## File location
|
||||
|
||||
The default path is `~/.fabro/user.toml`. Fabro silently skips loading if the file is missing.
|
||||
The default path is `~/.fabro/settings.toml`.
|
||||
|
||||
Use `fabro server start --config /path/to/settings.toml` if the server should read a different file.
|
||||
|
||||
## Who reads what
|
||||
|
||||
`settings.toml` uses the same schema as `fabro.toml` and `workflow.toml`, but each process only reads the fields it understands.
|
||||
|
||||
| Scope | Examples |
|
||||
|---|---|
|
||||
| CLI-only | `verbose`, `upgrade_check`, `[server]`, `[exec]`, `[mcp_servers]` |
|
||||
| Shared defaults | `[llm]`, `[log]`, `[git]`, `[pull_request]`, plus run-default sections like `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` |
|
||||
| Server-only | `storage_dir`, `max_concurrent_runs`, `[web]`, `[api]`, `[features]` |
|
||||
|
||||
See [Server Configuration](/administration/server-configuration) for the server-owned sections.
|
||||
|
||||
## Precedence
|
||||
|
||||
CLI flags always take the highest priority:
|
||||
For CLI commands running on the local machine, precedence is:
|
||||
|
||||
1. **CLI flags** — always win
|
||||
2. **`user.toml`** — used when no flag is provided
|
||||
3. **Built-in defaults** — used when neither flag nor config is set
|
||||
2. **`workflow.toml` / `run.toml`** — per-run overrides
|
||||
3. **`fabro.toml`** — project defaults
|
||||
4. **`settings.toml`** — machine defaults
|
||||
5. **Built-in defaults**
|
||||
|
||||
On a same-machine setup, the server reads the same `settings.toml`. On a remote setup, the CLI machine and server machine each use their own local `settings.toml`.
|
||||
|
||||
## Full example
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
verbose = true
|
||||
upgrade_check = true
|
||||
|
||||
|
|
@ -67,7 +91,7 @@ url = "https://mcp.sentry.dev/mcp"
|
|||
Authorization = "Bearer sk-xxx"
|
||||
```
|
||||
|
||||
All fields are optional. You can include just the sections and keys you want to override.
|
||||
All fields are optional. Include only the sections and keys you want to override. On a same-machine install, this same file can also include server sections such as `[web]` and `[api]`.
|
||||
|
||||
## `upgrade_check`
|
||||
|
||||
|
|
@ -140,7 +164,7 @@ Configure the default log level. Precedence: `FABRO_LOG` env var > `--debug` fla
|
|||
|
||||
### `[git.author]`
|
||||
|
||||
Customize the git author identity used for checkpoint commits. Overrides the server default when set.
|
||||
Customize the git author identity used for checkpoint commits. On same-machine setups, the CLI and server read the same `[git.author]` value. On remote setups, each machine uses its own local `settings.toml`.
|
||||
|
||||
| Key | Description | Default |
|
||||
|---|---|---|
|
||||
|
|
@ -149,7 +173,7 @@ Customize the git author identity used for checkpoint commits. Overrides the ser
|
|||
|
||||
## `[server]` section
|
||||
|
||||
Connection info for commands that can target a remote Fabro server.
|
||||
Connection info for commands that target a remote Fabro server.
|
||||
|
||||
| Key | Description | Default |
|
||||
|---|---|---|
|
||||
|
|
@ -175,7 +199,7 @@ Optional mTLS configuration for authenticating with the server. When present, th
|
|||
|
||||
Paths support `~/` expansion. Example:
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[server.tls]
|
||||
cert = "~/.fabro/tls/client.crt"
|
||||
key = "~/.fabro/tls/client.key"
|
||||
|
|
@ -184,9 +208,9 @@ ca = "~/.fabro/tls/ca.crt"
|
|||
|
||||
## `[pull_request]`
|
||||
|
||||
Enable auto-PR globally so workflows open a GitHub pull request on successful completion — even when running with a `.fabro` file instead of a `run.toml`.
|
||||
Enable auto-PR globally so workflows open a GitHub pull request on successful completion, even when running with a `.fabro` file instead of a `run.toml`.
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[pull_request]
|
||||
enabled = true
|
||||
```
|
||||
|
|
@ -195,7 +219,7 @@ enabled = true
|
|||
|---|---|---|
|
||||
| `enabled` | Automatically create a PR after successful runs | `false` |
|
||||
|
||||
Precedence: `run.toml` > `fabro.toml` (project config) > `user.toml` > `server.toml` > built-in default (`false`).
|
||||
Precedence: `run.toml` > `fabro.toml` > `settings.toml` > built-in default (`false`).
|
||||
|
||||
## `[mcp_servers]` section
|
||||
|
||||
|
|
@ -205,7 +229,7 @@ Configure [MCP servers](/agents/mcp) to connect to during `fabro exec` sessions.
|
|||
|
||||
Spawn a local process and communicate over stdin/stdout:
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[mcp_servers.filesystem]
|
||||
type = "stdio"
|
||||
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
|
||||
|
|
@ -228,7 +252,7 @@ NODE_ENV = "production"
|
|||
|
||||
Connect to a remote MCP server over Streamable HTTP:
|
||||
|
||||
```toml title="user.toml"
|
||||
```toml title="settings.toml"
|
||||
[mcp_servers.sentry]
|
||||
type = "http"
|
||||
url = "https://mcp.sentry.dev/mcp"
|
||||
|
|
@ -241,13 +265,13 @@ Authorization = "Bearer sk-xxx"
|
|||
|---|---|---|
|
||||
| `type` | Must be `"http"` | — |
|
||||
| `url` | The MCP server endpoint URL | — |
|
||||
| `headers` | Optional HTTP headers (e.g., for authentication) | `{}` |
|
||||
| `headers` | Optional HTTP headers (for example, for authentication) | `{}` |
|
||||
| `startup_timeout_secs` | Max seconds for the MCP handshake | `10` |
|
||||
| `tool_timeout_secs` | Max seconds for a single tool call | `60` |
|
||||
|
||||
### Sandbox transport
|
||||
|
||||
Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configured in [run config TOML](/execution/run-configuration#mcp_servers) rather than `user.toml`.
|
||||
Run an MCP server inside the workflow's sandbox and connect via preview URL. Only available with remote sandbox providers ([Daytona](/integrations/daytona)) that support port previews. Typically configure this in [run config TOML](/execution/run-configuration#mcp_servers) rather than `settings.toml`.
|
||||
|
||||
```toml title="run.toml"
|
||||
[mcp_servers.playwright]
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ struct Cli {
|
|||
pub use fabro_config::user::{OutputFormat, PermissionLevel};
|
||||
|
||||
impl AgentArgs {
|
||||
/// Fill `None` fields from user.toml values, then hardcoded defaults.
|
||||
/// Fill `None` fields from settings.toml values, then hardcoded defaults.
|
||||
pub fn apply_cli_defaults(
|
||||
&mut self,
|
||||
provider: Option<&str>,
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ use fabro_workflow::artifacts::{ArtifactEntry, scan_artifacts};
|
|||
use crate::args::{ArtifactCpArgs, GlobalArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{print_json_pretty, split_run_path};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn cp_command(args: &ArtifactCpArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let (run_id, asset_path) = parse_source(&args.source);
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(run_id)?;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ use fabro_workflow::artifacts::scan_artifacts;
|
|||
use crate::args::{ArtifactListArgs, GlobalArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::format_size;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn list_command(args: &ArtifactListArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run_id)?;
|
||||
let runtime_state = RuntimeState::new(&run.path);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ fn merged_config(workflow: Option<&Path>, args: &SettingsArgs) -> anyhow::Result
|
|||
Some(path) => ConfigLayer::for_workflow(path, &cwd)?,
|
||||
None => ConfigLayer::project(&cwd)?,
|
||||
};
|
||||
let cli = user_config::user_layer_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli = user_config::settings_layer_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
|
||||
base.combine(cli).resolve()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ use std::sync::LazyLock;
|
|||
use anyhow::Result;
|
||||
use fabro_api::types as api_types;
|
||||
use fabro_config::legacy_env;
|
||||
use fabro_config::user::{default_user_config_path, legacy_user_config_path};
|
||||
use fabro_config::user::{
|
||||
default_settings_path, legacy_old_user_config_path, legacy_server_config_path,
|
||||
legacy_user_config_path,
|
||||
};
|
||||
pub(crate) use fabro_util::check_report::{
|
||||
CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus,
|
||||
};
|
||||
|
|
@ -148,45 +151,52 @@ pub(crate) fn check_system_deps(specs: &[DepSpec], outcomes: &[ProbeOutcome]) ->
|
|||
}
|
||||
|
||||
pub(crate) fn check_config(
|
||||
user_path: Option<PathBuf>,
|
||||
legacy_path: Option<PathBuf>,
|
||||
settings_path: Option<PathBuf>,
|
||||
legacy_paths: &[PathBuf],
|
||||
) -> CheckResult {
|
||||
match (user_path, legacy_path) {
|
||||
(Some(path), None) => CheckResult {
|
||||
match (settings_path, legacy_paths.is_empty()) {
|
||||
(Some(path), true) => CheckResult {
|
||||
name: "Configuration".to_string(),
|
||||
status: CheckStatus::Pass,
|
||||
summary: path.display().to_string(),
|
||||
details: vec![CheckDetail::new(format!("Loaded from {}", path.display()))],
|
||||
remediation: None,
|
||||
},
|
||||
(Some(path), Some(legacy)) => CheckResult {
|
||||
(Some(path), false) => CheckResult {
|
||||
name: "Configuration".to_string(),
|
||||
status: CheckStatus::Warning,
|
||||
summary: path.display().to_string(),
|
||||
details: vec![
|
||||
CheckDetail::new(format!("Loaded from {}", path.display())),
|
||||
CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display())),
|
||||
],
|
||||
remediation: Some(format!("Delete or rename {}", legacy.display())),
|
||||
details: std::iter::once(CheckDetail::new(format!("Loaded from {}", path.display())))
|
||||
.chain(legacy_paths.iter().map(|legacy| {
|
||||
CheckDetail::new(format!("Ignoring legacy config file {}", legacy.display()))
|
||||
}))
|
||||
.collect(),
|
||||
remediation: Some("Delete or rename legacy config files".to_string()),
|
||||
},
|
||||
(None, Some(legacy)) => CheckResult {
|
||||
(None, false) => CheckResult {
|
||||
name: "Configuration".to_string(),
|
||||
status: CheckStatus::Warning,
|
||||
summary: "legacy config file ignored".to_string(),
|
||||
details: vec![
|
||||
CheckDetail::new(format!("Found legacy config file {}", legacy.display())),
|
||||
CheckDetail::new("Rename it to ~/.fabro/user.toml".to_string()),
|
||||
],
|
||||
remediation: Some(format!("Rename {} to ~/.fabro/user.toml", legacy.display())),
|
||||
summary: "legacy config files ignored".to_string(),
|
||||
details: legacy_paths
|
||||
.iter()
|
||||
.map(|legacy| {
|
||||
CheckDetail::new(format!("Found legacy config file {}", legacy.display()))
|
||||
})
|
||||
.chain(std::iter::once(CheckDetail::new(
|
||||
"Rename one to ~/.fabro/settings.toml or create a new settings.toml"
|
||||
.to_string(),
|
||||
)))
|
||||
.collect(),
|
||||
remediation: Some("Create ~/.fabro/settings.toml".to_string()),
|
||||
},
|
||||
(None, None) => CheckResult {
|
||||
(None, true) => CheckResult {
|
||||
name: "Configuration".to_string(),
|
||||
status: CheckStatus::Warning,
|
||||
summary: "no user config file found".to_string(),
|
||||
summary: "no settings config file found".to_string(),
|
||||
details: vec![CheckDetail::new(
|
||||
"Create ~/.fabro/user.toml to configure Fabro".to_string(),
|
||||
"Create ~/.fabro/settings.toml to configure Fabro".to_string(),
|
||||
)],
|
||||
remediation: Some("Create ~/.fabro/user.toml".to_string()),
|
||||
remediation: Some("Create ~/.fabro/settings.toml".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -304,8 +314,16 @@ pub(crate) async fn run_doctor(
|
|||
Some(spinner)
|
||||
};
|
||||
|
||||
let user_config_path = default_user_config_path();
|
||||
let legacy_config_path = legacy_user_config_path();
|
||||
let settings_config_path = default_settings_path();
|
||||
let legacy_config_paths = [
|
||||
legacy_user_config_path(),
|
||||
legacy_old_user_config_path(),
|
||||
legacy_server_config_path(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|path| path.exists())
|
||||
.collect::<Vec<_>>();
|
||||
let legacy_env_path = legacy_env::legacy_env_file_path()
|
||||
.ok()
|
||||
.filter(|path| path.exists());
|
||||
|
|
@ -316,8 +334,8 @@ pub(crate) async fn run_doctor(
|
|||
title: "Local".to_string(),
|
||||
checks: vec![
|
||||
check_config(
|
||||
user_config_path.filter(|path| path.exists()),
|
||||
legacy_config_path.filter(|path| path.exists()),
|
||||
settings_config_path.filter(|path| path.exists()),
|
||||
&legacy_config_paths,
|
||||
),
|
||||
check_legacy_env(legacy_env_path),
|
||||
],
|
||||
|
|
@ -430,21 +448,21 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn check_config_pass_with_path() {
|
||||
let result = check_config(Some(PathBuf::from("/home/user/.fabro/user.toml")), None);
|
||||
let result = check_config(Some(PathBuf::from("/home/user/.fabro/settings.toml")), &[]);
|
||||
assert_eq!(result.status, CheckStatus::Pass);
|
||||
assert!(result.summary.contains(".fabro/user.toml"));
|
||||
assert!(result.summary.contains(".fabro/settings.toml"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_config_warning_without_path() {
|
||||
let result = check_config(None, None);
|
||||
let result = check_config(None, &[]);
|
||||
assert_eq!(result.status, CheckStatus::Warning);
|
||||
assert!(result.remediation.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_config_warning_for_legacy_only_path() {
|
||||
let result = check_config(None, Some(PathBuf::from("/home/user/.fabro/cli.toml")));
|
||||
let result = check_config(None, &[PathBuf::from("/home/user/.fabro/cli.toml")]);
|
||||
assert_eq!(result.status, CheckStatus::Warning);
|
||||
assert!(result.summary.contains("legacy"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::args::{ExecArgs, GlobalArgs};
|
|||
use crate::user_config;
|
||||
|
||||
pub(crate) async fn execute(mut args: ExecArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = user_config::load_user_settings()?;
|
||||
let cli_settings = user_config::load_settings()?;
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled());
|
||||
let exec_defaults = cli_settings.exec.as_ref();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use crate::commands::run::output::api_diagnostics_to_local;
|
|||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
|
||||
use crate::server_client;
|
||||
use crate::shared::{absolute_or_current, print_diagnostics, print_json_pretty, relative_path};
|
||||
use crate::user_config::{self, load_settings_with_storage_dir};
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &GraphArgs,
|
||||
|
|
@ -21,6 +22,8 @@ pub(crate) async fn run(
|
|||
globals.require_no_json()?;
|
||||
}
|
||||
|
||||
let settings = load_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
let connection = user_config::server_backed_command_connection(&args.target, &settings)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
|
|
@ -29,7 +32,7 @@ pub(crate) async fn run(
|
|||
args: None,
|
||||
run_id: None,
|
||||
})?;
|
||||
let client = server_client::connect_server_backed(&args.target).await?;
|
||||
let client = server_client::connect_server_connection(&connection).await?;
|
||||
let preflight = client.run_preflight(built.manifest.clone()).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&preflight.workflow.diagnostics);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use dialoguer::theme::ColorfulTheme;
|
|||
use dialoguer::{MultiSelect, Select};
|
||||
use fabro_api::types::SetSecretRequest;
|
||||
use fabro_config::legacy_env;
|
||||
use fabro_config::user::USER_CONFIG_FILENAME;
|
||||
use fabro_config::user::SETTINGS_CONFIG_FILENAME;
|
||||
use fabro_model::Provider;
|
||||
use fabro_server::secret_store::SecretStore;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -187,26 +187,72 @@ fn generate_mtls_certs(dir: &Path) -> Result<()> {
|
|||
// Config TOML generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn root_table_mut(doc: &mut toml::Value) -> Result<&mut toml::Table> {
|
||||
doc.as_table_mut()
|
||||
.context("settings.toml root is not a table")
|
||||
}
|
||||
|
||||
fn ensure_table<'a>(table: &'a mut toml::Table, key: &str) -> Result<&'a mut toml::Table> {
|
||||
table
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(|| toml::Value::Table(toml::Table::default()))
|
||||
.as_table_mut()
|
||||
.with_context(|| format!("settings.toml [{key}] is not a table"))
|
||||
}
|
||||
|
||||
fn merge_server_settings(doc: &mut toml::Value, username: &str) -> Result<()> {
|
||||
let root = root_table_mut(doc)?;
|
||||
let web = ensure_table(root, "web")?;
|
||||
web.insert(
|
||||
"url".to_string(),
|
||||
toml::Value::String("http://localhost:3000".to_string()),
|
||||
);
|
||||
|
||||
let auth = ensure_table(web, "auth")?;
|
||||
auth.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("github".to_string()),
|
||||
);
|
||||
auth.insert(
|
||||
"allowed_usernames".to_string(),
|
||||
toml::Value::Array(vec![toml::Value::String(username.to_string())]),
|
||||
);
|
||||
|
||||
let api = ensure_table(root, "api")?;
|
||||
api.insert(
|
||||
"base_url".to_string(),
|
||||
toml::Value::String("https://localhost:3000/api/v1".to_string()),
|
||||
);
|
||||
api.insert(
|
||||
"authentication_strategies".to_string(),
|
||||
toml::Value::Array(vec![
|
||||
toml::Value::String("jwt".to_string()),
|
||||
toml::Value::String("mtls".to_string()),
|
||||
]),
|
||||
);
|
||||
|
||||
let tls = ensure_table(api, "tls")?;
|
||||
tls.insert(
|
||||
"cert".to_string(),
|
||||
toml::Value::String("~/.fabro/certs/server.crt".to_string()),
|
||||
);
|
||||
tls.insert(
|
||||
"key".to_string(),
|
||||
toml::Value::String("~/.fabro/certs/server.key".to_string()),
|
||||
);
|
||||
tls.insert(
|
||||
"ca".to_string(),
|
||||
toml::Value::String("~/.fabro/certs/ca.crt".to_string()),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn format_config_toml(username: &str) -> String {
|
||||
format!(
|
||||
r#"[web]
|
||||
url = "http://localhost:3000"
|
||||
|
||||
[web.auth]
|
||||
provider = "github"
|
||||
allowed_usernames = ["{username}"]
|
||||
|
||||
[api]
|
||||
# Public API base URL advertised by the server itself.
|
||||
base_url = "https://localhost:3000/api/v1"
|
||||
authentication_strategies = ["jwt", "mtls"]
|
||||
|
||||
[api.tls]
|
||||
cert = "~/.fabro/certs/server.crt"
|
||||
key = "~/.fabro/certs/server.key"
|
||||
ca = "~/.fabro/certs/ca.crt"
|
||||
"#
|
||||
)
|
||||
let mut doc = toml::Value::Table(toml::Table::default());
|
||||
merge_server_settings(&mut doc, username).expect("default server config should be valid");
|
||||
toml::to_string_pretty(&doc).expect("default server config should serialize")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -439,23 +485,23 @@ async fn setup_github_app(
|
|||
.context("missing 'pem' in GitHub response")?
|
||||
.to_string();
|
||||
|
||||
// Write non-secret config to user.toml
|
||||
let user_toml_path = fabro_dir.join(USER_CONFIG_FILENAME);
|
||||
// Write non-secret config to settings.toml
|
||||
let user_toml_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME);
|
||||
let existing = std::fs::read_to_string(&user_toml_path).unwrap_or_default();
|
||||
let mut doc: toml::Value = if existing.is_empty() {
|
||||
toml::Value::Table(toml::Table::default())
|
||||
} else {
|
||||
toml::from_str(&existing).context("failed to parse existing user.toml")?
|
||||
toml::from_str(&existing).context("failed to parse existing settings.toml")?
|
||||
};
|
||||
let table = doc
|
||||
.as_table_mut()
|
||||
.context("user.toml root is not a table")?;
|
||||
.context("settings.toml root is not a table")?;
|
||||
let git = table
|
||||
.entry("git")
|
||||
.or_insert(toml::Value::Table(toml::Table::default()));
|
||||
let git_table = git
|
||||
.as_table_mut()
|
||||
.context("user.toml [git] is not a table")?;
|
||||
.context("settings.toml [git] is not a table")?;
|
||||
git_table.insert("app_id".into(), toml::Value::String(app_id));
|
||||
git_table.insert("slug".into(), toml::Value::String(slug.clone()));
|
||||
git_table.insert("client_id".into(), toml::Value::String(client_id));
|
||||
|
|
@ -521,8 +567,7 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res
|
|||
let web_url = &args.web_url;
|
||||
let s = Styles::detect_stderr();
|
||||
let emoji = console::Emoji("⚒️ ", "");
|
||||
let cli_settings =
|
||||
user_config::load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = user_config::load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let storage_dir = cli_settings.storage_dir();
|
||||
let server_was_running = record::active_server_record(&storage_dir).is_some();
|
||||
|
||||
|
|
@ -693,7 +738,7 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res
|
|||
if setup_github {
|
||||
let github_env_pairs = setup_github_app(&fabro_dir, &s, web_url).await?;
|
||||
let slug = {
|
||||
let user_toml_path = fabro_dir.join(USER_CONFIG_FILENAME);
|
||||
let user_toml_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME);
|
||||
let toml_content = std::fs::read_to_string(&user_toml_path).unwrap_or_default();
|
||||
let doc: toml::Value = toml::from_str(&toml_content)
|
||||
.unwrap_or(toml::Value::Table(toml::Table::default()));
|
||||
|
|
@ -721,10 +766,10 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res
|
|||
eprintln!(" {}", s.dim.apply_to("─────────────────────"));
|
||||
eprintln!();
|
||||
|
||||
let config_path = fabro_dir.join("server.toml");
|
||||
let config_path = fabro_dir.join(SETTINGS_CONFIG_FILENAME);
|
||||
let write_config = if config_path.exists() {
|
||||
spawn_blocking(|| {
|
||||
prompt_confirm("~/.fabro/server.toml already exists. Overwrite?", false)
|
||||
prompt_confirm("~/.fabro/settings.toml already exists. Overwrite?", false)
|
||||
})
|
||||
.await??
|
||||
} else {
|
||||
|
|
@ -735,14 +780,20 @@ pub(crate) async fn run_install(args: &InstallArgs, globals: &GlobalArgs) -> Res
|
|||
let username: String =
|
||||
spawn_blocking(|| prompt_input("GitHub username for allowed access")).await??;
|
||||
|
||||
let toml_content = format_config_toml(&username);
|
||||
std::fs::write(&config_path, &toml_content)?;
|
||||
let existing = std::fs::read_to_string(&config_path).unwrap_or_default();
|
||||
let mut doc: toml::Value = if existing.is_empty() {
|
||||
toml::Value::Table(toml::Table::default())
|
||||
} else {
|
||||
toml::from_str(&existing).context("failed to parse existing settings.toml")?
|
||||
};
|
||||
merge_server_settings(&mut doc, &username)?;
|
||||
std::fs::write(&config_path, toml::to_string_pretty(&doc)?)?;
|
||||
eprintln!(
|
||||
" {}",
|
||||
s.dim.apply_to(format!("Wrote {}", config_path.display()))
|
||||
);
|
||||
} else {
|
||||
eprintln!(" {}", s.dim.apply_to("Keeping existing server.toml"));
|
||||
eprintln!(" {}", s.dim.apply_to("Keeping existing settings.toml"));
|
||||
}
|
||||
eprintln!();
|
||||
}
|
||||
|
|
@ -1005,6 +1056,69 @@ mod tests {
|
|||
assert_eq!(tls.ca, PathBuf::from("~/.fabro/certs/ca.crt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_server_settings_preserves_existing_git_table() {
|
||||
let mut doc: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[git]
|
||||
app_id = "123"
|
||||
|
||||
[git.author]
|
||||
name = "fabro"
|
||||
email = "fabro@example.com"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_server_settings(&mut doc, "alice").unwrap();
|
||||
|
||||
let git = doc.get("git").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(git.get("app_id").and_then(toml::Value::as_str), Some("123"));
|
||||
let author = git.get("author").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(
|
||||
author.get("name").and_then(toml::Value::as_str),
|
||||
Some("fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
author.get("email").and_then(toml::Value::as_str),
|
||||
Some("fabro@example.com")
|
||||
);
|
||||
assert_eq!(
|
||||
doc.get("web")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|web| web.get("auth"))
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|auth| auth.get("allowed_usernames"))
|
||||
.and_then(toml::Value::as_array)
|
||||
.and_then(|allowed| allowed.first())
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("alice")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_server_settings_preserves_existing_api_nested_keys() {
|
||||
let mut doc: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[api]
|
||||
base_url = "https://example.com/api/v1"
|
||||
|
||||
[api.extra]
|
||||
mode = "keep-me"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
merge_server_settings(&mut doc, "alice").unwrap();
|
||||
|
||||
let api = doc.get("api").and_then(toml::Value::as_table).unwrap();
|
||||
let extra = api.get("extra").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(
|
||||
extra.get("mode").and_then(toml::Value::as_str),
|
||||
Some("keep-me")
|
||||
);
|
||||
}
|
||||
|
||||
// -- GitHub App manifest --
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ pub(crate) async fn execute(command: Option<ModelsCommand>, globals: &GlobalArgs
|
|||
ModelsCommand::List(args) => &args.target,
|
||||
ModelsCommand::Test(args) => &args.target,
|
||||
};
|
||||
let cli_settings = user_config::load_user_settings_with_storage_dir(target_args.storage_dir())?;
|
||||
let cli_settings = user_config::load_settings_with_storage_dir(target_args.storage_dir())?;
|
||||
let connection = user_config::model_server_connection(target_args, &cli_settings)?;
|
||||
let client = server_client::connect_resolved_api_client(&connection).await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,14 @@ use tracing::info;
|
|||
|
||||
use crate::args::{GlobalArgs, PrCloseArgs};
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn close_command(
|
||||
args: PrCloseArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
close_from(&base, args, github_app, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ use crate::args::{GlobalArgs, PrCreateArgs};
|
|||
use crate::commands::store::rebuild::rebuild_run_store;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn create_command(
|
||||
args: PrCreateArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
create_from(&base, args, github_app, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::args::{GlobalArgs, PrListArgs};
|
|||
use crate::server_client;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PrRow {
|
||||
|
|
@ -27,7 +27,7 @@ pub(super) async fn list_command(
|
|||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
list_from(
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ use fabro_workflow::run_lookup::runs_base;
|
|||
|
||||
use crate::args::{GlobalArgs, PrMergeArgs};
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn merge_command(
|
||||
args: PrMergeArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
merge_from(&base, args, github_app, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,32 +13,32 @@ use fabro_types::PullRequestRecord;
|
|||
use crate::args::{GlobalArgs, PrCommand, PrNamespace};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::github::build_github_app_credentials;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn dispatch(ns: PrNamespace, globals: &GlobalArgs) -> Result<()> {
|
||||
match ns.command {
|
||||
PrCommand::Create(args) => {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id())?;
|
||||
Box::pin(create::create_command(args, github_app, globals)).await
|
||||
}
|
||||
PrCommand::List(args) => {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id())?;
|
||||
list::list_command(args, github_app, globals).await
|
||||
}
|
||||
PrCommand::View(args) => {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id())?;
|
||||
view::view_command(args, github_app, globals).await
|
||||
}
|
||||
PrCommand::Merge(args) => {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id())?;
|
||||
merge::merge_command(args, github_app, globals).await
|
||||
}
|
||||
PrCommand::Close(args) => {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let github_app = build_github_app_credentials(cli_settings.app_id())?;
|
||||
close::close_command(args, github_app, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ use fabro_workflow::run_lookup::runs_base;
|
|||
|
||||
use crate::args::{GlobalArgs, PrViewArgs};
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(super) async fn view_command(
|
||||
args: PrViewArgs,
|
||||
github_app: Option<fabro_github::GitHubAppCredentials>,
|
||||
globals: &GlobalArgs,
|
||||
) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
view_from(&base, args, github_app, globals).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,13 @@ use crate::commands::run::output::{
|
|||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest, preflight_manifest_args};
|
||||
use crate::server_client;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::{self, load_settings_with_storage_dir};
|
||||
|
||||
pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> anyhow::Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
args.verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
let connection = user_config::server_backed_command_connection(&args.target, &cli_settings)?;
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
let manifest = build_run_manifest(ManifestBuildInput {
|
||||
|
|
@ -24,7 +25,7 @@ pub(crate) async fn execute(mut args: PreflightArgs, globals: &GlobalArgs) -> an
|
|||
args: preflight_manifest_args(&args),
|
||||
run_id: None,
|
||||
})?;
|
||||
let client = server_client::connect_server_backed(&args.target).await?;
|
||||
let client = server_client::connect_server_connection(&connection).await?;
|
||||
let response = client.run_preflight(manifest.manifest).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ use fabro_util::terminal::Styles;
|
|||
use crate::args::{GlobalArgs, RunArgs};
|
||||
use crate::server_client;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::{self, user_layer_with_storage_dir};
|
||||
use crate::user_config::{self, settings_layer_with_storage_dir};
|
||||
|
||||
pub(crate) async fn execute(mut args: RunArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli_settings = user_config::load_user_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
let cli = user_layer_with_storage_dir(args.target.storage_dir())?;
|
||||
let cli_settings = user_config::load_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
let cli = settings_layer_with_storage_dir(args.target.storage_dir())?;
|
||||
args.verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
|
||||
let quiet = args.detach;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use tracing::{debug, info};
|
|||
use crate::args::{CpArgs, GlobalArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{print_json_pretty, split_run_path};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
enum CopyDirection {
|
||||
Download {
|
||||
|
|
@ -26,7 +26,7 @@ enum CopyDirection {
|
|||
|
||||
pub(crate) async fn cp_command(args: CpArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let direction = parse_direction(&args.src, &args.dst)?;
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
|
||||
match direction {
|
||||
CopyDirection::Download {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ pub(crate) async fn create_run(
|
|||
args: run_manifest_args(args),
|
||||
run_id,
|
||||
})?;
|
||||
|
||||
let connection = user_config::server_backed_command_connection(&args.target, &settings)?;
|
||||
let client = server_client::connect_server_connection(&connection).await?;
|
||||
if !quiet {
|
||||
|
|
|
|||
|
|
@ -10,11 +10,11 @@ use crate::args::{DiffArgs, GlobalArgs};
|
|||
use crate::server_client::RunProjection;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn run(args: DiffArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
info!(run_id = %args.run, "Showing diff");
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ use crate::args::{ForkArgs, GlobalArgs};
|
|||
use crate::commands::store::rebuild::rebuild_run_store;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run_id)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use fabro_util::terminal::Styles;
|
|||
use crate::args::{AttachArgs, GlobalArgs, RunArgs, RunCommands, RunnerArgs, StartArgs};
|
||||
use crate::server_runs::ServerSummaryLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::user_layer_with_storage_dir;
|
||||
use crate::user_config::settings_layer_with_storage_dir;
|
||||
|
||||
pub(crate) mod attach;
|
||||
pub(crate) mod command;
|
||||
|
|
@ -39,7 +39,7 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
|
|||
RunCommands::Create(mut args) => {
|
||||
apply_json_defaults(&mut args, globals);
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
let cli = user_layer_with_storage_dir(args.target.storage_dir())?;
|
||||
let cli = settings_layer_with_storage_dir(args.target.storage_dir())?;
|
||||
let created_run = Box::pin(create::create_run(&args, cli, styles, true)).await?;
|
||||
if globals.json {
|
||||
print_json_pretty(&serde_json::json!({ "run_id": created_run.run_id }))?;
|
||||
|
|
@ -90,8 +90,9 @@ pub(crate) async fn dispatch(cmd: RunCommands, globals: &GlobalArgs) -> Result<(
|
|||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
#[cfg(feature = "sleep_inhibitor")]
|
||||
let _sleep_guard = {
|
||||
let cli_settings =
|
||||
load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = crate::user_config::load_settings_with_storage_dir(
|
||||
args.storage_dir.as_deref(),
|
||||
)?;
|
||||
crate::sleep_inhibitor::guard(cli_settings.prevent_idle_sleep_enabled())
|
||||
};
|
||||
resume::resume_command(args, styles, globals).await
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ use tracing::info;
|
|||
use crate::args::{GlobalArgs, PreviewArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{print_json_pretty, validate_daytona_provider};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn run(args: PreviewArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run)?;
|
||||
let record = lookup
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use fabro_util::terminal::Styles;
|
|||
use crate::args::{GlobalArgs, ResumeArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::print_json_pretty;
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
/// Resume an interrupted workflow run.
|
||||
///
|
||||
|
|
@ -15,7 +15,7 @@ pub(crate) async fn resume_command(
|
|||
styles: &'static Styles,
|
||||
globals: &GlobalArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::commands::store::rebuild::rebuild_run_store;
|
|||
use crate::server_client::ServerStoreClient;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{color_if, print_json_pretty};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct TimelineEntryJson {
|
||||
|
|
@ -30,7 +30,7 @@ pub(crate) struct TimelineEntryJson {
|
|||
|
||||
pub(crate) async fn run(args: &RewindArgs, styles: &Styles, globals: &GlobalArgs) -> Result<()> {
|
||||
let repo = Repository::discover(".").context("not in a git repository")?;
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run_id)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use fabro_types::{RunId, RunStatus};
|
|||
use tokio::time::sleep;
|
||||
|
||||
use crate::server_client;
|
||||
use crate::user_config::load_user_settings;
|
||||
use crate::user_config::load_settings;
|
||||
|
||||
pub(crate) async fn execute(
|
||||
run_id: RunId,
|
||||
|
|
@ -17,7 +17,7 @@ pub(crate) async fn execute(
|
|||
|
||||
let storage_dir = match storage_dir {
|
||||
Some(storage_dir) => storage_dir,
|
||||
None => load_user_settings()?.storage_dir(),
|
||||
None => load_settings()?.storage_dir(),
|
||||
};
|
||||
|
||||
let client = server_client::connect_server(&storage_dir).await?;
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ use tracing::info;
|
|||
use crate::args::{GlobalArgs, SshArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{print_json_pretty, validate_daytona_provider};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn run(args: SshArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
if globals.json && !args.print {
|
||||
globals.require_no_json()?;
|
||||
}
|
||||
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R
|
|||
foreground,
|
||||
serve_args,
|
||||
}) => {
|
||||
let settings =
|
||||
user_config::load_user_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let storage_dir = settings.storage_dir();
|
||||
let bind_addr = match serve_args.bind.as_deref() {
|
||||
Some(s) => bind::parse_bind(s)?,
|
||||
|
|
@ -37,15 +36,13 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R
|
|||
storage_dir,
|
||||
timeout,
|
||||
}) => {
|
||||
let settings =
|
||||
user_config::load_user_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let storage_dir = settings.storage_dir();
|
||||
stop::execute(&storage_dir, Duration::from_secs(timeout));
|
||||
Ok(())
|
||||
}
|
||||
ServerCommand::Status(ServerStatusArgs { storage_dir, json }) => {
|
||||
let settings =
|
||||
user_config::load_user_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let storage_dir = settings.storage_dir();
|
||||
status::execute(&storage_dir, json)
|
||||
}
|
||||
|
|
@ -59,8 +56,7 @@ pub(crate) async fn dispatch(command: ServerCommand, _globals: &GlobalArgs) -> R
|
|||
} else {
|
||||
// __serve should always receive an explicit --bind from the parent,
|
||||
// but fall back to the storage dir default if missing.
|
||||
let settings =
|
||||
user_config::load_user_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
Bind::Unix(settings.storage_dir().join("fabro.sock"))
|
||||
};
|
||||
let styles: &'static Styles = Box::leak(Box::new(Styles::detect_stderr()));
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ use crate::args::{GlobalArgs, StoreDumpArgs};
|
|||
use crate::commands::store::rebuild::rebuild_run_store;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{absolute_or_current, print_json_pretty};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
pub(crate) async fn dump_command(args: &StoreDumpArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
let run = lookup.resolve(&args.run)?;
|
||||
let run_id = run.run_id();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use fabro_workflow::run_status::RunStatus;
|
|||
use crate::args::{DfArgs, GlobalArgs};
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{format_size, print_json_pretty};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SummaryRow {
|
||||
|
|
@ -45,7 +45,7 @@ struct DfOutput {
|
|||
}
|
||||
|
||||
pub(super) async fn df_command(args: &DfArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let data_dir = cli_settings.storage_dir();
|
||||
let runs_base_dir = runs_base(&data_dir);
|
||||
let logs_base_dir = logs_base(&data_dir);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::commands::runs::rm::remove_run_with_cleanup;
|
|||
use crate::server_client;
|
||||
use crate::server_runs::ServerRunLookup;
|
||||
use crate::shared::{format_size, print_json_pretty};
|
||||
use crate::user_config::load_user_settings_with_storage_dir;
|
||||
use crate::user_config::load_settings_with_storage_dir;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PruneRunRow {
|
||||
|
|
@ -23,7 +23,7 @@ struct PruneRunRow {
|
|||
}
|
||||
|
||||
pub(super) async fn prune_command(args: &RunsPruneArgs, globals: &GlobalArgs) -> Result<()> {
|
||||
let cli_settings = load_user_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let cli_settings = load_settings_with_storage_dir(args.storage_dir.as_deref())?;
|
||||
let base = runs_base(&cli_settings.storage_dir());
|
||||
let lookup = ServerRunLookup::connect(&cli_settings.storage_dir()).await?;
|
||||
prune_from(args, lookup.client(), lookup.summaries(), &base, globals).await
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ use crate::commands::run::output::api_diagnostics_to_local;
|
|||
use crate::manifest_builder::{ManifestBuildInput, build_run_manifest};
|
||||
use crate::server_client;
|
||||
use crate::shared::{print_diagnostics, print_json_pretty, relative_path};
|
||||
use crate::user_config::{self, load_settings_with_storage_dir};
|
||||
|
||||
pub(crate) async fn run(
|
||||
args: &ValidateArgs,
|
||||
styles: &Styles,
|
||||
globals: &GlobalArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
let settings = load_settings_with_storage_dir(args.target.storage_dir())?;
|
||||
let connection = user_config::server_backed_command_connection(&args.target, &settings)?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let built = build_run_manifest(ManifestBuildInput {
|
||||
workflow: args.workflow.clone(),
|
||||
|
|
@ -21,7 +24,7 @@ pub(crate) async fn run(
|
|||
args: None,
|
||||
run_id: None,
|
||||
})?;
|
||||
let client = server_client::connect_server_backed(&args.target).await?;
|
||||
let client = server_client::connect_server_connection(&connection).await?;
|
||||
let response = client.run_preflight(built.manifest).await?;
|
||||
let diagnostics = api_diagnostics_to_local(&response.workflow.diagnostics);
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ mod user_config;
|
|||
use anyhow::Result;
|
||||
use args::{Commands, GlobalArgs, LONG_VERSION, RunCommands, ServerCommand, ServerNamespace};
|
||||
use clap::{CommandFactory, FromArgMatches, Parser, error::ErrorKind, parser::ValueSource};
|
||||
use fabro_config::server::load_server_settings;
|
||||
use fabro_config::user::load_settings_config;
|
||||
use fabro_telemetry::{git, panic as tel_panic, sanitize, sender};
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
|
|
@ -229,7 +229,9 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
}),
|
||||
}) = command.as_ref()
|
||||
{
|
||||
match load_server_settings(args.config.as_deref()) {
|
||||
match load_settings_config(args.config.as_deref())
|
||||
.and_then(fabro_types::Settings::try_from)
|
||||
{
|
||||
Ok(server_settings) => (
|
||||
server_settings.log.as_ref().and_then(|l| l.level.clone()),
|
||||
false,
|
||||
|
|
@ -237,7 +239,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
} else {
|
||||
match user_config::load_user_settings() {
|
||||
match user_config::load_settings() {
|
||||
Ok(cli_settings) => (
|
||||
cli_settings.log.as_ref().and_then(|l| l.level.clone()),
|
||||
cli_settings.upgrade_check_enabled(),
|
||||
|
|
@ -295,7 +297,7 @@ async fn main_inner() -> (String, Result<()>) {
|
|||
commands::server::dispatch(ns.command, &globals).await?;
|
||||
}
|
||||
Commands::Doctor(args) => {
|
||||
let cli_settings = user_config::load_user_settings()?;
|
||||
let cli_settings = user_config::load_settings()?;
|
||||
let verbose = args.verbose || cli_settings.verbose_enabled();
|
||||
let exit_code = commands::doctor::run_doctor(&args, verbose, &globals).await?;
|
||||
std::process::exit(exit_code);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use fabro_config::ConfigLayer;
|
|||
use fabro_config::project::{self, discover_project_config, resolve_workflow_path};
|
||||
use fabro_config::run::parse_run_config;
|
||||
use fabro_config::sandbox::DockerfileSource;
|
||||
use fabro_config::user::default_user_config_path;
|
||||
use fabro_config::user::default_settings_path;
|
||||
use fabro_graphviz::graph::AttrValue;
|
||||
use fabro_graphviz::parser;
|
||||
use fabro_sandbox::daytona::detect_repo_info;
|
||||
|
|
@ -45,7 +45,7 @@ struct WorkflowScanInput {
|
|||
}
|
||||
|
||||
pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManifest> {
|
||||
let user_layer = ConfigLayer::user()?;
|
||||
let user_layer = ConfigLayer::settings()?;
|
||||
let merged_settings = input
|
||||
.args_layer
|
||||
.clone()
|
||||
|
|
@ -86,7 +86,7 @@ pub(crate) fn build_run_manifest(input: ManifestBuildInput) -> Result<BuiltManif
|
|||
type_: types::ManifestConfigType::Project,
|
||||
});
|
||||
}
|
||||
if let Some(path) = default_user_config_path().filter(|path| path.is_file()) {
|
||||
if let Some(path) = default_settings_path().filter(|path| path.is_file()) {
|
||||
let source = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
configs.push(types::ManifestConfig {
|
||||
|
|
|
|||
|
|
@ -106,14 +106,6 @@ pub(crate) async fn connect_server(storage_dir: &Path) -> Result<ServerStoreClie
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_server_backed(
|
||||
args: &ServerConnectionArgs,
|
||||
) -> Result<ServerStoreClient> {
|
||||
Ok(ServerStoreClient {
|
||||
client: connect_server_backed_api_client(args).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_server_connection(
|
||||
connection: &user_config::ServerConnection,
|
||||
) -> Result<ServerStoreClient> {
|
||||
|
|
@ -124,7 +116,7 @@ pub(crate) async fn connect_server_connection(
|
|||
|
||||
pub(crate) async fn connect_server_only(args: &ServerTargetArgs) -> Result<ServerStoreClient> {
|
||||
let storage_dir = std::env::var_os("FABRO_STORAGE_DIR").map(std::path::PathBuf::from);
|
||||
let settings = user_config::load_user_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(storage_dir.as_deref())?;
|
||||
let connection = user_config::server_only_command_connection(args, &settings)?;
|
||||
connect_server_connection(&connection).await
|
||||
}
|
||||
|
|
@ -160,7 +152,7 @@ pub(crate) async fn connect_resolved_api_client(
|
|||
pub(crate) async fn connect_server_backed_api_client(
|
||||
args: &ServerConnectionArgs,
|
||||
) -> Result<fabro_api::Client> {
|
||||
let settings = user_config::load_user_settings_with_storage_dir(args.storage_dir())?;
|
||||
let settings = user_config::load_settings_with_storage_dir(args.storage_dir())?;
|
||||
let connection = user_config::server_backed_command_connection(args, &settings)?;
|
||||
connect_resolved_api_client(&connection).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,21 +9,21 @@ use tracing::debug;
|
|||
|
||||
use crate::args::{ServerConnectionArgs, ServerTargetArgs};
|
||||
|
||||
pub(crate) fn load_user_settings() -> anyhow::Result<Settings> {
|
||||
ConfigLayer::user()?.resolve()
|
||||
pub(crate) fn load_settings() -> anyhow::Result<Settings> {
|
||||
ConfigLayer::settings()?.resolve()
|
||||
}
|
||||
|
||||
pub(crate) fn user_layer_with_storage_dir(
|
||||
pub(crate) fn settings_layer_with_storage_dir(
|
||||
storage_dir: Option<&Path>,
|
||||
) -> anyhow::Result<ConfigLayer> {
|
||||
let layer = ConfigLayer::user()?;
|
||||
let layer = ConfigLayer::settings()?;
|
||||
Ok(apply_storage_dir_override(layer, storage_dir))
|
||||
}
|
||||
|
||||
pub(crate) fn load_user_settings_with_storage_dir(
|
||||
pub(crate) fn load_settings_with_storage_dir(
|
||||
storage_dir: Option<&Path>,
|
||||
) -> anyhow::Result<Settings> {
|
||||
user_layer_with_storage_dir(storage_dir)?.resolve()
|
||||
settings_layer_with_storage_dir(storage_dir)?.resolve()
|
||||
}
|
||||
|
||||
pub(crate) fn apply_storage_dir_override(
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ fn attach_uses_configured_server_target_without_server_flag() {
|
|||
.body(r#"{"data":[],"meta":{"has_more":false}}"#);
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ fn parse_settings(stdout: &[u8]) -> Settings {
|
|||
/// Uses `context.home_dir` for the home directory. Returns project tempdir.
|
||||
fn setup_settings_fixture(context: &fabro_test::TestContext) -> tempfile::TempDir {
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
r#"
|
||||
verbose = true
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ SHARED = "run"
|
|||
project
|
||||
}
|
||||
|
||||
/// Set up an external workflow fixture with a custom storage_dir in user.toml.
|
||||
/// Set up an external workflow fixture with a custom storage_dir in settings.toml.
|
||||
/// Returns (project_tempdir, storage_dir_path).
|
||||
fn setup_external_workflow_fixture(
|
||||
context: &mut fabro_test::TestContext,
|
||||
|
|
@ -191,7 +191,7 @@ fn setup_external_workflow_fixture(
|
|||
context.manage_storage_dir(&storage_dir);
|
||||
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
r#"
|
||||
storage_dir = "{}"
|
||||
|
|
@ -373,7 +373,7 @@ fn settings_explicit_workflow_path_uses_workflow_project_layers() {
|
|||
let cwd = tempfile::tempdir().unwrap();
|
||||
let workflow = project.path().join("workflow.toml");
|
||||
|
||||
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from user.toml
|
||||
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
|
||||
let output = context
|
||||
.settings()
|
||||
.env_remove("FABRO_STORAGE_DIR")
|
||||
|
|
@ -409,7 +409,7 @@ fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
|
|||
let workflow = project.path().join("workflow.toml");
|
||||
let run_id = unique_run_id();
|
||||
|
||||
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from user.toml
|
||||
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
|
||||
context
|
||||
.command()
|
||||
.env_remove("FABRO_STORAGE_DIR")
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ fn create_uses_configured_server_target_without_server_flag() {
|
|||
.body(run_status_response(run_id.as_str(), "submitted").to_string());
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ fn create_storage_dir_suppresses_configured_server_target() {
|
|||
&context.test_case_id()[..8]
|
||||
));
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
@ -178,7 +178,7 @@ fn create_cli_server_target_overrides_configured_server_target() {
|
|||
.body(run_status_response(run_id.as_str(), "submitted").to_string());
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
"[server]\ntarget = \"{}/api/v1\"\n",
|
||||
config_server.base_url()
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ fn exec_missing_api_key_exits_with_error() {
|
|||
fn exec_uses_user_config_defaults() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
"[exec]\nprovider = \"openai\"\nmodel = \"gpt-4.1-mini\"\npermissions = \"read-only\"\noutput_format = \"json\"\n",
|
||||
);
|
||||
|
||||
|
|
@ -163,7 +163,7 @@ fn exec_configured_server_target_alone_does_not_reroute_exec() {
|
|||
then.status(500).body("config-should-not-be-used");
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ fn exec_cli_server_target_overrides_configured_server_target() {
|
|||
then.status(500).body("cli-override-marker");
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
"[server]\ntarget = \"{}/api/v1\"\n",
|
||||
config_server.base_url()
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ fn list_uses_configured_server_target_without_server_flag() {
|
|||
);
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ fn ps_uses_configured_server_target_without_server_flag() {
|
|||
);
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ fn rm_uses_configured_server_target_without_local_run_dir() {
|
|||
then.status(204);
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ fn detach_uses_configured_server_target_without_server_flag() {
|
|||
.body(run_status_response(run_id.as_str(), "queued").to_string());
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
@ -252,7 +252,7 @@ fn detach_storage_dir_suppresses_configured_server_target() {
|
|||
let local_storage =
|
||||
std::path::PathBuf::from(format!("/tmp/fabro-run-{}", &context.test_case_id()[..8]));
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!("[server]\ntarget = \"{}/api/v1\"\n", server.base_url()),
|
||||
);
|
||||
|
||||
|
|
@ -311,7 +311,7 @@ fn detach_cli_server_target_overrides_configured_server_target() {
|
|||
.body(run_status_response(run_id.as_str(), "queued").to_string());
|
||||
});
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
format!(
|
||||
"[server]\ntarget = \"{}/api/v1\"\n",
|
||||
config_server.base_url()
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ fn runner_uses_snapshotted_app_id_for_github_credentials() {
|
|||
let workflow_path = context.temp_dir.join("workflow.fabro");
|
||||
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
"\
|
||||
version = 1
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ digraph GitHubApp {
|
|||
"#
|
||||
);
|
||||
|
||||
context.write_home(".fabro/user.toml", "version = 1\n");
|
||||
context.write_home(".fabro/settings.toml", "version = 1\n");
|
||||
|
||||
let mut cmd = context.command();
|
||||
cmd.env("GITHUB_APP_PRIVATE_KEY", "%%%not-base64%%%");
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ fn help() {
|
|||
--max-concurrent-runs <MAX_CONCURRENT_RUNS>
|
||||
Maximum number of concurrent run executions
|
||||
--config <CONFIG>
|
||||
Path to server config file (default: ~/.fabro/server.toml)
|
||||
Path to server config file (default: ~/.fabro/settings.toml)
|
||||
-h, --help
|
||||
Print help
|
||||
----- stderr -----
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ fn conclusion_status(context: &fabro_test::TestContext) -> String {
|
|||
async fn hook_prompt_proceed_allows_run() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
&format!(
|
||||
r#"
|
||||
[[hooks]]
|
||||
|
|
@ -126,7 +126,7 @@ model = "{model}"
|
|||
async fn hook_prompt_block_prevents_run() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
&format!(
|
||||
r#"
|
||||
[[hooks]]
|
||||
|
|
@ -182,7 +182,7 @@ model = "{model}"
|
|||
async fn hook_agent_proceed_allows_run() {
|
||||
let context = test_context!();
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
&format!(
|
||||
r#"
|
||||
[[hooks]]
|
||||
|
|
@ -234,7 +234,7 @@ async fn hook_agent_with_tool_use() {
|
|||
let marker = context.temp_dir.join("hook_check.txt");
|
||||
std::fs::write(&marker, "READY").unwrap();
|
||||
context.write_home(
|
||||
".fabro/user.toml",
|
||||
".fabro/settings.toml",
|
||||
&format!(
|
||||
r#"
|
||||
[[hooks]]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use crate::run::{
|
|||
ArtifactsConfig, CheckpointConfig, GitHubConfig, LlmConfig, PullRequestConfig, SetupConfig,
|
||||
};
|
||||
use crate::sandbox::SandboxConfig;
|
||||
use crate::server::{self, ApiConfig, FeaturesConfig, GitConfig, LogConfig, WebConfig};
|
||||
use crate::server::{ApiConfig, FeaturesConfig, GitConfig, LogConfig, WebConfig};
|
||||
use crate::user::{self, ExecConfig, ServerConfig};
|
||||
use fabro_types::Settings;
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ fn is_default_checkpoint(c: &CheckpointConfig) -> bool {
|
|||
|
||||
/// Unified sparse configuration type for all Fabro config sources.
|
||||
///
|
||||
/// Loading functions (`load_user_config`, `load_server_config`, `load_run_config`,
|
||||
/// Loading functions (`load_settings_config`, `load_run_config`,
|
||||
/// `parse_project_config`) all return this type. Fields irrelevant to a
|
||||
/// particular source are left unset (`None` / empty).
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
|
|
@ -218,14 +218,9 @@ impl ConfigLayer {
|
|||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Load user defaults from `~/.fabro/user.toml`.
|
||||
pub fn user() -> anyhow::Result<Self> {
|
||||
user::load_user_config(None)
|
||||
}
|
||||
|
||||
/// Load server defaults from `~/.fabro/server.toml`.
|
||||
pub fn server() -> anyhow::Result<Self> {
|
||||
server::load_server_config(None)
|
||||
/// Load machine-level defaults from `~/.fabro/settings.toml`.
|
||||
pub fn settings() -> anyhow::Result<Self> {
|
||||
user::load_settings_config(None)
|
||||
}
|
||||
|
||||
/// Convert this combined config layer into final resolved settings.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::ConfigLayer;
|
||||
use fabro_types::Settings;
|
||||
pub use fabro_types::settings::server::{
|
||||
ApiAuthStrategy, ApiSettings, AuthProvider, AuthSettings, FeaturesSettings, GitAuthorSettings,
|
||||
|
|
@ -179,16 +178,6 @@ impl From<LogConfig> for LogSettings {
|
|||
}
|
||||
}
|
||||
|
||||
/// Load server config from an explicit path or `~/.fabro/server.toml`, returning defaults if the
|
||||
/// default file doesn't exist. An explicit path that doesn't exist is an error.
|
||||
pub fn load_server_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
||||
crate::load_config_file(path, "server.toml")
|
||||
}
|
||||
|
||||
pub fn load_server_settings(path: Option<&Path>) -> anyhow::Result<Settings> {
|
||||
load_server_config(path)?.try_into()
|
||||
}
|
||||
|
||||
/// Resolve the storage directory: config value > default `~/.fabro`.
|
||||
pub fn resolve_storage_dir(settings: &Settings) -> PathBuf {
|
||||
settings.storage_dir()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ pub use fabro_types::settings::user::{
|
|||
ClientTlsSettings, ExecSettings, OutputFormat, PermissionLevel, ServerSettings,
|
||||
};
|
||||
|
||||
pub const USER_CONFIG_FILENAME: &str = "user.toml";
|
||||
pub const SETTINGS_CONFIG_FILENAME: &str = "settings.toml";
|
||||
pub const LEGACY_USER_CONFIG_FILENAME: &str = "cli.toml";
|
||||
pub const LEGACY_OLD_USER_CONFIG_FILENAME: &str = "user.toml";
|
||||
pub const LEGACY_SERVER_CONFIG_FILENAME: &str = "server.toml";
|
||||
|
||||
static WARNED_LEGACY_USER_CONFIGS: OnceLock<Mutex<HashSet<PathBuf>>> = OnceLock::new();
|
||||
|
||||
|
|
@ -77,14 +79,22 @@ impl From<ExecConfig> for ExecSettings {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn default_user_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(USER_CONFIG_FILENAME))
|
||||
pub fn default_settings_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(SETTINGS_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
pub fn legacy_user_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_USER_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
pub fn legacy_old_user_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_OLD_USER_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
pub fn legacy_server_config_path() -> Option<PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".fabro").join(LEGACY_SERVER_CONFIG_FILENAME))
|
||||
}
|
||||
|
||||
fn warned_legacy_user_configs() -> &'static Mutex<HashSet<PathBuf>> {
|
||||
WARNED_LEGACY_USER_CONFIGS.get_or_init(|| Mutex::new(HashSet::new()))
|
||||
}
|
||||
|
|
@ -96,18 +106,25 @@ fn should_warn_about_legacy_user_config(path: &Path) -> bool {
|
|||
.insert(path.to_path_buf())
|
||||
}
|
||||
|
||||
/// Load user config from an explicit path or `~/.fabro/user.toml`, returning defaults if the
|
||||
/// Load settings config from an explicit path or `~/.fabro/settings.toml`, returning defaults if the
|
||||
/// default file doesn't exist. An explicit path that doesn't exist is an error.
|
||||
#[allow(clippy::print_stderr)]
|
||||
pub fn load_user_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
||||
pub fn load_settings_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
||||
if let Some(explicit) = path {
|
||||
return crate::load_config_file(Some(explicit), USER_CONFIG_FILENAME);
|
||||
return crate::load_config_file(Some(explicit), SETTINGS_CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
if let Some(legacy_path) = legacy_user_config_path() {
|
||||
for legacy_path in [
|
||||
legacy_user_config_path(),
|
||||
legacy_old_user_config_path(),
|
||||
legacy_server_config_path(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if legacy_path.is_file() && should_warn_about_legacy_user_config(&legacy_path) {
|
||||
let target = default_user_config_path()
|
||||
.unwrap_or_else(|| PathBuf::from(format!("~/.fabro/{USER_CONFIG_FILENAME}")));
|
||||
let target = default_settings_path()
|
||||
.unwrap_or_else(|| PathBuf::from(format!("~/.fabro/{SETTINGS_CONFIG_FILENAME}")));
|
||||
eprintln!(
|
||||
"Warning: ignoring legacy config file {}. Rename it to {}.",
|
||||
legacy_path.display(),
|
||||
|
|
@ -116,12 +133,17 @@ pub fn load_user_config(path: Option<&Path>) -> anyhow::Result<ConfigLayer> {
|
|||
}
|
||||
}
|
||||
|
||||
crate::load_config_file(None, USER_CONFIG_FILENAME)
|
||||
crate::load_config_file(None, SETTINGS_CONFIG_FILENAME)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_warn_about_legacy_user_config;
|
||||
use super::{
|
||||
LEGACY_OLD_USER_CONFIG_FILENAME, LEGACY_SERVER_CONFIG_FILENAME,
|
||||
LEGACY_USER_CONFIG_FILENAME, SETTINGS_CONFIG_FILENAME, default_settings_path,
|
||||
legacy_old_user_config_path, legacy_server_config_path, legacy_user_config_path,
|
||||
should_warn_about_legacy_user_config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn should_warn_about_legacy_user_config_once_per_path() {
|
||||
|
|
@ -133,4 +155,40 @@ mod tests {
|
|||
assert!(!should_warn_about_legacy_user_config(&first));
|
||||
assert!(should_warn_about_legacy_user_config(&second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_paths_use_expected_filenames() {
|
||||
let home = dirs::home_dir().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
default_settings_path(),
|
||||
Some(home.join(".fabro").join(SETTINGS_CONFIG_FILENAME))
|
||||
);
|
||||
assert_eq!(
|
||||
legacy_user_config_path(),
|
||||
Some(home.join(".fabro").join(LEGACY_USER_CONFIG_FILENAME))
|
||||
);
|
||||
assert_eq!(
|
||||
legacy_old_user_config_path(),
|
||||
Some(home.join(".fabro").join(LEGACY_OLD_USER_CONFIG_FILENAME))
|
||||
);
|
||||
assert_eq!(
|
||||
legacy_server_config_path(),
|
||||
Some(home.join(".fabro").join(LEGACY_SERVER_CONFIG_FILENAME))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_warn_once_per_legacy_path_even_with_multiple_filenames() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let user = dir.path().join("user.toml");
|
||||
let server = dir.path().join("server.toml");
|
||||
let cli = dir.path().join("cli.toml");
|
||||
|
||||
assert!(should_warn_about_legacy_user_config(&user));
|
||||
assert!(!should_warn_about_legacy_user_config(&user));
|
||||
assert!(should_warn_about_legacy_user_config(&server));
|
||||
assert!(!should_warn_about_legacy_user_config(&server));
|
||||
assert!(should_warn_about_legacy_user_config(&cli));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ async fn check_github_app(state: &AppState) -> CheckResult {
|
|||
status: CheckStatus::Error,
|
||||
summary: "missing app_id".to_string(),
|
||||
details: Vec::new(),
|
||||
remediation: Some("Set git.app_id in server.toml".to_string()),
|
||||
remediation: Some("Set git.app_id in settings.toml".to_string()),
|
||||
};
|
||||
};
|
||||
let Some(private_key_raw) = private_key_raw else {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ pub(crate) struct PreparedManifest {
|
|||
pub working_directory: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_manifest(
|
||||
pub(crate) fn prepare_manifest_with_mode(
|
||||
server_settings: &Settings,
|
||||
manifest: &types::RunManifest,
|
||||
local_daemon_mode: bool,
|
||||
) -> Result<PreparedManifest> {
|
||||
if manifest.version != 1 {
|
||||
bail!("unsupported manifest version {}", manifest.version);
|
||||
|
|
@ -70,7 +71,11 @@ pub(crate) fn prepare_manifest(
|
|||
.try_fold(ConfigLayer::default(), |layer, config| {
|
||||
Ok::<_, anyhow::Error>(parse_manifest_config(config)?.combine(layer))
|
||||
})?;
|
||||
let server_defaults = server_defaults_layer(server_settings)?;
|
||||
let server_defaults = if local_daemon_mode {
|
||||
local_daemon_server_overrides_layer(server_settings)?
|
||||
} else {
|
||||
server_defaults_layer(server_settings)?
|
||||
};
|
||||
|
||||
let mut settings = args_layer
|
||||
.combine(workflow_layer)
|
||||
|
|
@ -233,6 +238,18 @@ fn server_defaults_layer(settings: &Settings) -> Result<ConfigLayer> {
|
|||
Ok(layer)
|
||||
}
|
||||
|
||||
fn local_daemon_server_overrides_layer(settings: &Settings) -> Result<ConfigLayer> {
|
||||
let layer = server_defaults_layer(settings)?;
|
||||
Ok(ConfigLayer {
|
||||
storage_dir: layer.storage_dir,
|
||||
max_concurrent_runs: layer.max_concurrent_runs,
|
||||
web: layer.web,
|
||||
api: layer.api,
|
||||
features: layer.features,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_server_owned_fields(layer: &mut ConfigLayer) {
|
||||
layer.server = None;
|
||||
layer.exec = None;
|
||||
|
|
@ -790,7 +807,8 @@ mod tests {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let prepared = prepare_manifest(&server_settings, &minimal_manifest()).unwrap();
|
||||
let prepared =
|
||||
prepare_manifest_with_mode(&server_settings, &minimal_manifest(), false).unwrap();
|
||||
|
||||
assert_eq!(prepared.settings.dry_run, None);
|
||||
assert_eq!(
|
||||
|
|
@ -819,8 +837,67 @@ mod tests {
|
|||
verbose: None,
|
||||
});
|
||||
|
||||
let prepared = prepare_manifest(&server_settings, &manifest).unwrap();
|
||||
let prepared = prepare_manifest_with_mode(&server_settings, &manifest, false).unwrap();
|
||||
|
||||
assert_eq!(prepared.settings.dry_run, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_manifest_local_daemon_prefers_bundled_settings_without_duplication() {
|
||||
let server_settings: Settings = toml::from_str(
|
||||
r#"
|
||||
storage_dir = "/srv/fabro"
|
||||
|
||||
[setup]
|
||||
commands = ["cli-setup"]
|
||||
|
||||
[git]
|
||||
app_id = "snapshotted-app-id"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut manifest = minimal_manifest();
|
||||
manifest.workflows.get_mut("workflow.fabro").unwrap().config =
|
||||
Some(types::ManifestWorkflowConfig {
|
||||
path: "workflow.toml".to_string(),
|
||||
source: r#"
|
||||
version = 1
|
||||
|
||||
[setup]
|
||||
commands = ["workflow-setup"]
|
||||
"#
|
||||
.to_string(),
|
||||
});
|
||||
manifest.configs.push(types::ManifestConfig {
|
||||
path: Some("/tmp/home/.fabro/settings.toml".to_string()),
|
||||
source: Some(
|
||||
r#"
|
||||
[setup]
|
||||
commands = ["cli-setup"]
|
||||
|
||||
[git]
|
||||
app_id = "snapshotted-app-id"
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
type_: types::ManifestConfigType::User,
|
||||
});
|
||||
|
||||
let prepared = prepare_manifest_with_mode(&server_settings, &manifest, true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
prepared
|
||||
.settings
|
||||
.setup
|
||||
.as_ref()
|
||||
.map(|setup| setup.commands.clone()),
|
||||
Some(vec!["workflow-setup".to_string(), "cli-setup".to_string(),])
|
||||
);
|
||||
assert_eq!(prepared.settings.app_id(), Some("snapshotted-app-id"));
|
||||
assert_eq!(
|
||||
prepared.settings.storage_dir,
|
||||
Some(PathBuf::from("/srv/fabro"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use fabro_config::server::{load_server_settings, resolve_storage_dir};
|
||||
use fabro_config::server::resolve_storage_dir;
|
||||
use fabro_config::user::{default_settings_path, load_settings_config};
|
||||
use fabro_util::terminal::Styles;
|
||||
use object_store::local::LocalFileSystem;
|
||||
use tokio::net::{TcpListener, UnixListener};
|
||||
|
|
@ -48,11 +49,21 @@ pub struct ServeArgs {
|
|||
#[arg(long)]
|
||||
pub max_concurrent_runs: Option<usize>,
|
||||
|
||||
/// Path to server config file (default: ~/.fabro/server.toml)
|
||||
/// Path to server config file (default: ~/.fabro/settings.toml)
|
||||
#[arg(long)]
|
||||
pub config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn load_settings(path: Option<&Path>) -> anyhow::Result<Settings> {
|
||||
load_settings_config(path)?.try_into()
|
||||
}
|
||||
|
||||
fn resolved_config_path(path: Option<&Path>) -> PathBuf {
|
||||
path.map(Path::to_path_buf)
|
||||
.or_else(default_settings_path)
|
||||
.unwrap_or_else(|| PathBuf::from(".fabro/settings.toml"))
|
||||
}
|
||||
|
||||
fn apply_serve_overrides(base: &Settings, args: &ServeArgs, dry_run_mode: bool) -> Settings {
|
||||
let mut settings = base.clone();
|
||||
if dry_run_mode {
|
||||
|
|
@ -93,7 +104,8 @@ pub async fn serve_command(
|
|||
storage_dir_override: Option<PathBuf>,
|
||||
) -> anyhow::Result<()> {
|
||||
let config_path = args.config.clone();
|
||||
let disk_settings = load_server_settings(config_path.as_deref())?;
|
||||
let disk_settings = load_settings(config_path.as_deref())?;
|
||||
let active_config_path = resolved_config_path(config_path.as_deref());
|
||||
let data_dir = storage_dir_override.unwrap_or_else(|| resolve_storage_dir(&disk_settings));
|
||||
let secret_store_path = data_dir.join("secrets.json");
|
||||
let secret_store = SecretStore::load(secret_store_path.clone())?;
|
||||
|
|
@ -169,6 +181,8 @@ pub async fn serve_command(
|
|||
max_concurrent_runs,
|
||||
store,
|
||||
secret_store_path,
|
||||
active_config_path,
|
||||
matches!(&auth_mode, AuthMode::Disabled),
|
||||
)?;
|
||||
spawn_scheduler(Arc::clone(&state));
|
||||
let router = build_router(Arc::clone(&state), auth_mode);
|
||||
|
|
@ -249,7 +263,7 @@ pub async fn serve_command(
|
|||
interval.tick().await; // skip first immediate tick
|
||||
loop {
|
||||
interval.tick().await;
|
||||
match load_server_settings(config_path_for_poll.as_deref()) {
|
||||
match load_settings(config_path_for_poll.as_deref()) {
|
||||
Ok(new_disk_settings) => {
|
||||
let effective = apply_runtime_settings(
|
||||
&new_disk_settings,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@ pub struct AppState {
|
|||
pub sessions: SessionStore,
|
||||
pub(crate) secret_store: AsyncRwLock<SecretStore>,
|
||||
pub(crate) settings: Arc<RwLock<Settings>>,
|
||||
pub(crate) config_path: PathBuf,
|
||||
pub(crate) local_daemon_mode: bool,
|
||||
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
|
||||
}
|
||||
|
||||
|
|
@ -849,6 +851,8 @@ pub fn create_app_state_with_settings_and_registry_factory(
|
|||
5,
|
||||
test_store(),
|
||||
test_secret_store_path(),
|
||||
test_config_path(),
|
||||
false,
|
||||
)
|
||||
.expect("test app state should build")
|
||||
}
|
||||
|
|
@ -884,6 +888,8 @@ pub fn create_app_state_with_store(
|
|||
max_concurrent_runs,
|
||||
store,
|
||||
test_secret_store_path(),
|
||||
test_config_path(),
|
||||
false,
|
||||
)
|
||||
.expect("test app state should build")
|
||||
}
|
||||
|
|
@ -894,6 +900,8 @@ pub(crate) fn build_app_state_with_path(
|
|||
max_concurrent_runs: usize,
|
||||
store: StoreHandle,
|
||||
secret_store_path: PathBuf,
|
||||
config_path: PathBuf,
|
||||
local_daemon_mode: bool,
|
||||
) -> anyhow::Result<Arc<AppState>> {
|
||||
let secret_store = SecretStore::load(secret_store_path)?;
|
||||
Ok(Arc::new(AppState {
|
||||
|
|
@ -905,6 +913,8 @@ pub(crate) fn build_app_state_with_path(
|
|||
sessions: new_session_store(),
|
||||
secret_store: AsyncRwLock::new(secret_store),
|
||||
settings,
|
||||
config_path,
|
||||
local_daemon_mode,
|
||||
registry_factory_override,
|
||||
}))
|
||||
}
|
||||
|
|
@ -913,6 +923,10 @@ fn test_secret_store_path() -> PathBuf {
|
|||
std::env::temp_dir().join(format!("fabro-test-secrets-{}.json", Ulid::new()))
|
||||
}
|
||||
|
||||
fn test_config_path() -> PathBuf {
|
||||
std::env::temp_dir().join(format!("fabro-test-settings-{}.toml", Ulid::new()))
|
||||
}
|
||||
|
||||
async fn list_board_runs(
|
||||
_auth: AuthenticatedService,
|
||||
State(state): State<Arc<AppState>>,
|
||||
|
|
@ -1134,7 +1148,11 @@ async fn create_run(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RunManifest>,
|
||||
) -> Response {
|
||||
let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) {
|
||||
let prepared = match run_manifest::prepare_manifest_with_mode(
|
||||
&state.settings.read().unwrap(),
|
||||
&req,
|
||||
state.local_daemon_mode,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
|
|
@ -1191,7 +1209,11 @@ async fn run_preflight(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RunManifest>,
|
||||
) -> Response {
|
||||
let prepared = match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req) {
|
||||
let prepared = match run_manifest::prepare_manifest_with_mode(
|
||||
&state.settings.read().unwrap(),
|
||||
&req,
|
||||
state.local_daemon_mode,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
|
|
@ -1217,11 +1239,14 @@ async fn render_graph_from_manifest(
|
|||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<RenderWorkflowGraphRequest>,
|
||||
) -> Response {
|
||||
let prepared =
|
||||
match run_manifest::prepare_manifest(&state.settings.read().unwrap(), &req.manifest) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let prepared = match run_manifest::prepare_manifest_with_mode(
|
||||
&state.settings.read().unwrap(),
|
||||
&req.manifest,
|
||||
state.local_daemon_mode,
|
||||
) {
|
||||
Ok(prepared) => prepared,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
};
|
||||
let validated = match run_manifest::validate_prepared_manifest(&prepared) {
|
||||
Ok(validated) => validated,
|
||||
Err(err) => return ApiError::bad_request(err.to_string()).into_response(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
|
|
@ -488,10 +489,7 @@ async fn setup_register(
|
|||
);
|
||||
};
|
||||
|
||||
let settings_path = dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".fabro")
|
||||
.join("server.toml");
|
||||
let settings_path = state.config_path.clone();
|
||||
|
||||
let mut settings = state
|
||||
.settings
|
||||
|
|
@ -508,11 +506,33 @@ async fn setup_register(
|
|||
if let Some(parent) = settings_path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let toml = build_server_toml(&settings, &git);
|
||||
if let Err(error) = std::fs::write(&settings_path, toml) {
|
||||
let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
|
||||
let mut doc: toml::Value = if existing.is_empty() {
|
||||
toml::Value::Table(toml::Table::default())
|
||||
} else {
|
||||
match toml::from_str(&existing).context("failed to parse existing settings config") {
|
||||
Ok(doc) => doc,
|
||||
Err(error) => {
|
||||
return json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
json!({"error": format!("Failed to parse settings config: {error}")}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Err(error) = merge_settings_keys(&mut doc, &settings, &git) {
|
||||
return json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
json!({"error": format!("Failed to write server config: {error}")}),
|
||||
json!({"error": format!("Failed to update settings config: {error}")}),
|
||||
);
|
||||
}
|
||||
if let Err(error) = std::fs::write(
|
||||
&settings_path,
|
||||
toml::to_string_pretty(&doc).unwrap_or_default(),
|
||||
) {
|
||||
return json_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
json!({"error": format!("Failed to write settings config: {error}")}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -544,7 +564,24 @@ async fn setup_register(
|
|||
Json(json!({"ok": true, "restart_required": true})).into_response()
|
||||
}
|
||||
|
||||
fn build_server_toml(settings: &Settings, git: &GitSettings) -> String {
|
||||
fn root_table_mut(doc: &mut toml::Value) -> anyhow::Result<&mut toml::Table> {
|
||||
doc.as_table_mut()
|
||||
.ok_or_else(|| anyhow!("settings config root is not a table"))
|
||||
}
|
||||
|
||||
fn ensure_table<'a>(table: &'a mut toml::Table, key: &str) -> anyhow::Result<&'a mut toml::Table> {
|
||||
table
|
||||
.entry(key.to_string())
|
||||
.or_insert_with(|| toml::Value::Table(toml::Table::default()))
|
||||
.as_table_mut()
|
||||
.ok_or_else(|| anyhow!("settings config [{key}] is not a table"))
|
||||
}
|
||||
|
||||
fn merge_settings_keys(
|
||||
doc: &mut toml::Value,
|
||||
settings: &Settings,
|
||||
git: &GitSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
let web_url = settings.web.as_ref().map_or_else(
|
||||
|| "http://localhost:3000".to_string(),
|
||||
|web| web.url.clone(),
|
||||
|
|
@ -556,73 +593,136 @@ fn build_server_toml(settings: &Settings, git: &GitSettings) -> String {
|
|||
.unwrap_or_default();
|
||||
let api = settings.api.clone().unwrap_or_default();
|
||||
|
||||
let mut value = toml::Table::new();
|
||||
value.insert(
|
||||
"web".to_string(),
|
||||
toml::Value::Table({
|
||||
let mut web = toml::Table::new();
|
||||
web.insert("url".to_string(), toml::Value::String(web_url));
|
||||
web.insert(
|
||||
"auth".to_string(),
|
||||
toml::Value::Table({
|
||||
let mut auth = toml::Table::new();
|
||||
auth.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("github".to_string()),
|
||||
);
|
||||
auth.insert(
|
||||
"allowed_usernames".to_string(),
|
||||
toml::Value::Array(allowed.into_iter().map(toml::Value::String).collect()),
|
||||
);
|
||||
auth
|
||||
}),
|
||||
);
|
||||
web
|
||||
}),
|
||||
let root = root_table_mut(doc)?;
|
||||
let web = ensure_table(root, "web")?;
|
||||
web.insert("url".to_string(), toml::Value::String(web_url));
|
||||
let auth = ensure_table(web, "auth")?;
|
||||
auth.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("github".to_string()),
|
||||
);
|
||||
value.insert(
|
||||
"api".to_string(),
|
||||
toml::Value::Table({
|
||||
let mut api_table = toml::Table::new();
|
||||
api_table.insert("base_url".to_string(), toml::Value::String(api.base_url));
|
||||
api_table.insert(
|
||||
"authentication_strategies".to_string(),
|
||||
toml::Value::Array(
|
||||
api.authentication_strategies
|
||||
.iter()
|
||||
.map(|strategy| match strategy {
|
||||
ApiAuthStrategy::Jwt => "jwt",
|
||||
ApiAuthStrategy::Mtls => "mtls",
|
||||
})
|
||||
.map(|value| toml::Value::String(value.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
api_table
|
||||
}),
|
||||
auth.insert(
|
||||
"allowed_usernames".to_string(),
|
||||
toml::Value::Array(allowed.into_iter().map(toml::Value::String).collect()),
|
||||
);
|
||||
value.insert(
|
||||
"git".to_string(),
|
||||
toml::Value::Table({
|
||||
let mut git_table = toml::Table::new();
|
||||
git_table.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("github".to_string()),
|
||||
);
|
||||
git_table.insert(
|
||||
"app_id".to_string(),
|
||||
toml::Value::String(git.app_id.clone().unwrap_or_default()),
|
||||
);
|
||||
git_table.insert(
|
||||
"client_id".to_string(),
|
||||
toml::Value::String(git.client_id.clone().unwrap_or_default()),
|
||||
);
|
||||
git_table.insert(
|
||||
"slug".to_string(),
|
||||
toml::Value::String(git.slug.clone().unwrap_or_default()),
|
||||
);
|
||||
git_table
|
||||
}),
|
||||
|
||||
let api_table = ensure_table(root, "api")?;
|
||||
api_table.insert("base_url".to_string(), toml::Value::String(api.base_url));
|
||||
api_table.insert(
|
||||
"authentication_strategies".to_string(),
|
||||
toml::Value::Array(
|
||||
api.authentication_strategies
|
||||
.iter()
|
||||
.map(|strategy| match strategy {
|
||||
ApiAuthStrategy::Jwt => "jwt",
|
||||
ApiAuthStrategy::Mtls => "mtls",
|
||||
})
|
||||
.map(|value| toml::Value::String(value.to_string()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
toml::to_string(&value).unwrap_or_default()
|
||||
|
||||
let git_table = ensure_table(root, "git")?;
|
||||
git_table.insert(
|
||||
"provider".to_string(),
|
||||
toml::Value::String("github".to_string()),
|
||||
);
|
||||
git_table.insert(
|
||||
"app_id".to_string(),
|
||||
toml::Value::String(git.app_id.clone().unwrap_or_default()),
|
||||
);
|
||||
git_table.insert(
|
||||
"client_id".to_string(),
|
||||
toml::Value::String(git.client_id.clone().unwrap_or_default()),
|
||||
);
|
||||
git_table.insert(
|
||||
"slug".to_string(),
|
||||
toml::Value::String(git.slug.clone().unwrap_or_default()),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::merge_settings_keys;
|
||||
use fabro_types::Settings;
|
||||
|
||||
#[test]
|
||||
fn merge_settings_keys_preserves_unrelated_git_nested_keys() {
|
||||
let mut doc: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[git]
|
||||
provider = "github"
|
||||
|
||||
[git.author]
|
||||
name = "fabro"
|
||||
email = "fabro@example.com"
|
||||
|
||||
[git.webhooks]
|
||||
strategy = "tailscale_funnel"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings.web.get_or_insert_default().auth.allowed_usernames = vec!["alice".to_string()];
|
||||
settings.git.get_or_insert_default().provider = fabro_config::server::GitProvider::Github;
|
||||
settings.git.get_or_insert_default().app_id = Some("123".to_string());
|
||||
settings.git.get_or_insert_default().client_id = Some("abc".to_string());
|
||||
settings.git.get_or_insert_default().slug = Some("fabro".to_string());
|
||||
|
||||
merge_settings_keys(&mut doc, &settings, settings.git.as_ref().unwrap()).unwrap();
|
||||
|
||||
let git = doc.get("git").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(git.get("app_id").and_then(toml::Value::as_str), Some("123"));
|
||||
let author = git.get("author").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(
|
||||
author.get("name").and_then(toml::Value::as_str),
|
||||
Some("fabro")
|
||||
);
|
||||
let webhooks = git.get("webhooks").and_then(toml::Value::as_table).unwrap();
|
||||
assert_eq!(
|
||||
webhooks.get("strategy").and_then(toml::Value::as_str),
|
||||
Some("tailscale_funnel")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_settings_keys_preserves_unrelated_top_level_sections() {
|
||||
let mut doc: toml::Value = toml::from_str(
|
||||
r#"
|
||||
[exec]
|
||||
provider = "anthropic"
|
||||
|
||||
[server]
|
||||
target = "https://fabro.example.com/api/v1"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings.web.get_or_insert_default().auth.allowed_usernames = vec!["alice".to_string()];
|
||||
settings.git.get_or_insert_default().provider = fabro_config::server::GitProvider::Github;
|
||||
settings.git.get_or_insert_default().app_id = Some("123".to_string());
|
||||
settings.git.get_or_insert_default().client_id = Some("abc".to_string());
|
||||
settings.git.get_or_insert_default().slug = Some("fabro".to_string());
|
||||
|
||||
merge_settings_keys(&mut doc, &settings, settings.git.as_ref().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
doc.get("exec")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|exec| exec.get("provider"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("anthropic")
|
||||
);
|
||||
assert_eq!(
|
||||
doc.get("server")
|
||||
.and_then(toml::Value::as_table)
|
||||
.and_then(|server| server.get("target"))
|
||||
.and_then(toml::Value::as_str),
|
||||
Some("https://fabro.example.com/api/v1")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,9 +136,9 @@ async fn create_env_with_github_app(
|
|||
}
|
||||
|
||||
fn load_github_app_credentials() -> fabro_github::GitHubAppCredentials {
|
||||
// Read app_id from ~/.fabro/server.toml
|
||||
// Read app_id from ~/.fabro/settings.toml
|
||||
let home = dirs::home_dir().expect("No home directory");
|
||||
let config_path = home.join(".fabro/server.toml");
|
||||
let config_path = home.join(".fabro/settings.toml");
|
||||
let config_str = std::fs::read_to_string(&config_path)
|
||||
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", config_path.display()));
|
||||
|
||||
|
|
@ -152,11 +152,11 @@ fn load_github_app_credentials() -> fabro_github::GitHubAppCredentials {
|
|||
app_id: Option<String>,
|
||||
}
|
||||
|
||||
let config: Config = toml::from_str(&config_str).expect("Failed to parse server.toml");
|
||||
let config: Config = toml::from_str(&config_str).expect("Failed to parse settings.toml");
|
||||
let app_id = config
|
||||
.git
|
||||
.app_id
|
||||
.expect("app_id not set in server.toml [git] section");
|
||||
.expect("app_id not set in settings.toml [git] section");
|
||||
|
||||
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").expect("GITHUB_APP_PRIVATE_KEY not set");
|
||||
let private_key_pem = if raw.starts_with("-----") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue