Merge branch 'dense-default-fabro-settings'

This commit is contained in:
Bryan Helmkamp 2026-04-12 19:06:41 -04:00
commit 7d01c4e42b
21 changed files with 1252 additions and 366 deletions

View file

@ -1510,10 +1510,23 @@ paths:
operationId: retrieveServerSettings
tags: [Settings]
summary: Retrieve Server Settings
description: Returns the structured server settings.
description: >
Returns the server settings view selected by the optional `view` query
parameter. `view=layer` (the default) returns the current sparse
redacted `SettingsLayer` payload. `view=resolved` returns the server's
dense resolved settings payload after applying the same redaction
policy.
parameters:
- $ref: "#/components/parameters/SettingsView"
responses:
"200":
description: Server settings
headers:
X-Fabro-Settings-View:
description: Present with value `resolved` when the response body is the dense resolved settings view.
schema:
type: string
enum: [resolved]
content:
application/json:
schema:
@ -1549,6 +1562,16 @@ components:
type: string
example: 01JNQVR7M0EJ5GKAT2SC4ERS1Z
SettingsView:
name: view
in: query
required: false
description: Selects the server settings representation to return.
schema:
type: string
enum: [layer, resolved]
default: layer
StageId:
name: stageId
in: path
@ -4046,22 +4069,24 @@ components:
ServerSettings:
description: |
Non-secret view of the server's effective v2 settings.
Redacted server settings payload.
Wire shape mirrors `fabro_types::settings::SettingsFile` with the
secret-bearing subtrees dropped before serialization:
The `/api/v1/settings` endpoint supports two response shapes:
- `server.listen.*` (bind address, TLS key material)
- `server.auth.api.{jwt,mtls}` internals
- `server.artifacts.s3` / `server.slatedb.s3` credentials
- `server.integrations.github.webhooks`, Slack/Discord/Teams tokens
- Every `{{ env.NAME }}` InterpString is serialized in its unresolved
template form, never the resolved secret value.
- `view=layer` (default): the sparse redacted `SettingsLayer` shape
- `view=resolved`: the dense resolved `Settings` shape
The top-level object keys follow the v2 schema: `_version`, `project`,
`workflow`, `run`, `cli`, `server`, `features`.
Both views drop the same exact secret-bearing paths:
See `lib/crates/fabro-types/src/settings/tree.rs` for the full type.
- `server.listen`
- `server.auth.api.jwt.issuer`
- `server.auth.api.jwt.audience`
- `server.auth.api.mtls.ca`
- `server.auth.web.providers.github.client_secret`
For non-redacted `InterpString` fields, the wire payload preserves the
unresolved source/template string rather than any environment-resolved
secret value.
type: object
additionalProperties: true

View file

@ -2,7 +2,7 @@ use std::io::Write;
use std::path::Path;
use fabro_config::effective_settings::{EffectiveSettingsLayers, EffectiveSettingsMode};
use fabro_config::{effective_settings, load_settings_project, project};
use fabro_config::{load_and_resolve, load_settings_project, project};
use fabro_types::settings::SettingsLayer;
use fabro_util::printer::Printer;
@ -57,30 +57,53 @@ fn workflow_and_project_layers(
Ok((workflow_layer, project_layer))
}
async fn merged_config(args: &SettingsArgs, printer: Printer) -> anyhow::Result<SettingsLayer> {
fn strip_nulls(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for child in map.values_mut() {
strip_nulls(child);
}
map.retain(|_, child| !child.is_null());
}
serde_json::Value::Array(values) => {
for child in values {
strip_nulls(child);
}
}
_ => {}
}
}
fn local_settings_value(
args: &SettingsArgs,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
let base_ctx = CommandContext::base(printer)?;
let layers = config_layers(&base_ctx, args.workflow.as_deref())?;
if args.local {
return Ok(effective_settings::resolve_settings(
layers,
None,
EffectiveSettingsMode::LocalOnly,
)?);
}
let ctx = CommandContext::for_target(&args.target, printer)?;
let target = user_config::resolve_server_target(&args.target, ctx.machine_settings())?;
let server_settings = ctx.server().await?.retrieve_server_settings().await?;
let mode = match target {
user_config::ServerTarget::HttpUrl { .. } => EffectiveSettingsMode::RemoteServer,
user_config::ServerTarget::UnixSocket(_) => EffectiveSettingsMode::LocalDaemon,
};
Ok(effective_settings::resolve_settings(
let mut value = serde_json::to_value(load_and_resolve(
layers,
Some(&server_settings),
mode,
)?)
None,
EffectiveSettingsMode::LocalOnly,
)?)?;
strip_nulls(&mut value);
Ok(value)
}
async fn rendered_config(
args: &SettingsArgs,
printer: Printer,
) -> anyhow::Result<serde_json::Value> {
if args.local {
return local_settings_value(args, printer);
}
if args.workflow.is_some() {
anyhow::bail!("WORKFLOW requires --local; use `fabro settings --local WORKFLOW`");
}
let ctx = CommandContext::for_target(&args.target, printer)?;
ctx.server()
.await?
.retrieve_resolved_server_settings()
.await
}
pub(crate) async fn execute(
@ -88,7 +111,7 @@ pub(crate) async fn execute(
globals: &GlobalArgs,
printer: Printer,
) -> anyhow::Result<()> {
let config = Box::pin(merged_config(args, printer)).await?;
let config = Box::pin(rendered_config(args, printer)).await?;
if globals.json {
print_json_pretty(&config)?;
return Ok(());

View file

@ -279,16 +279,43 @@ impl ServerStoreClient {
&self.base_url
}
pub(crate) async fn retrieve_server_settings(&self) -> Result<SettingsLayer> {
let response = self
.client
.retrieve_server_settings()
.send()
pub(crate) async fn retrieve_resolved_server_settings(&self) -> Result<serde_json::Value> {
let url = format!("{}/api/v1/settings?view=resolved", self.base_url);
let response = self.http_client.get(&url).send().await?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&body) {
if let Some(detail) = value
.get("errors")
.and_then(serde_json::Value::as_array)
.and_then(|errors| errors.first())
.and_then(|entry| entry.get("detail"))
.and_then(serde_json::Value::as_str)
{
bail!("{detail}");
}
}
if body.is_empty() {
bail!("request failed with status {status}");
}
bail!("request failed with status {status}: {body}");
}
let marker = response
.headers()
.get("x-fabro-settings-view")
.and_then(|value| value.to_str().ok());
if marker != Some("resolved") {
bail!(
"server does not support resolved settings view; upgrade the server or use --local"
);
}
response
.json::<serde_json::Value>()
.await
.map_err(map_api_error)?;
let raw = serde_json::Value::Object(response.into_inner().into());
serde_json::from_value::<SettingsLayer>(raw)
.context("server returned a settings payload that does not match the v2 schema")
.context("server returned invalid JSON for the resolved settings view")
}
pub(crate) async fn create_run_from_manifest(

View file

@ -31,104 +31,104 @@ fn old_config_show_command_is_rejected() {
// Helpers
// ---------------------------------------------------------------------------
fn parse_settings(stdout: &[u8]) -> SettingsLayer {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML SettingsLayer")
fn parse_settings(stdout: &[u8]) -> serde_json::Value {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML settings")
}
fn resolve_cli(settings: &SettingsLayer) -> fabro_types::settings::CliSettings {
fabro_config::resolve_cli_from_file(settings).expect("cli settings should resolve")
fn parse_settings_json(stdout: &[u8]) -> serde_json::Value {
serde_json::from_slice(stdout).expect("stdout should be valid JSON settings")
}
fn resolve_project(settings: &SettingsLayer) -> fabro_types::settings::ProjectSettings {
fabro_config::resolve_project_from_file(settings).expect("project settings should resolve")
fn run_goal_inline(settings: &serde_json::Value) -> Option<&str> {
let goal = settings.get("run")?.get("goal")?;
(goal.get("type")?.as_str() == Some("inline"))
.then(|| goal.get("value")?.as_str())
.flatten()
}
fn resolve_run(settings: &SettingsLayer) -> fabro_types::settings::RunSettings {
fabro_config::resolve_run_from_file(settings).expect("run settings should resolve")
fn run_model_name(settings: &serde_json::Value) -> Option<&str> {
settings.get("run")?.get("model")?.get("name")?.as_str()
}
fn resolve_server(settings: &SettingsLayer) -> fabro_types::settings::ServerSettings {
fabro_config::resolve_server_from_file(settings).expect("server settings should resolve")
fn run_model_provider(settings: &serde_json::Value) -> Option<&str> {
settings.get("run")?.get("model")?.get("provider")?.as_str()
}
fn run_goal_inline(settings: &SettingsLayer) -> Option<String> {
match resolve_run(settings).goal {
Some(fabro_types::settings::run::RunGoal::Inline(value)) => Some(value.as_source()),
_ => None,
}
}
fn run_model_name(settings: &SettingsLayer) -> Option<String> {
resolve_run(settings)
.model
.name
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
}
fn run_model_provider(settings: &SettingsLayer) -> Option<String> {
resolve_run(settings)
.model
.provider
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
}
fn run_inputs(settings: &SettingsLayer) -> &std::collections::HashMap<String, toml::Value> {
fn run_inputs(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
settings
.run
.as_ref()
.and_then(|run| run.inputs.as_ref())
.get("run")
.and_then(|run| run.get("inputs"))
.and_then(serde_json::Value::as_object)
.expect("run.inputs")
}
fn run_sandbox(settings: &SettingsLayer) -> &fabro_types::settings::run::RunSandboxLayer {
fn run_sandbox(settings: &serde_json::Value) -> &serde_json::Value {
settings
.run
.as_ref()
.and_then(|run| run.sandbox.as_ref())
.get("run")
.and_then(|run| run.get("sandbox"))
.expect("run.sandbox")
}
fn run_checkpoint(settings: &SettingsLayer) -> &fabro_types::settings::run::RunCheckpointLayer {
fn run_checkpoint(settings: &serde_json::Value) -> &serde_json::Value {
settings
.run
.as_ref()
.and_then(|run| run.checkpoint.as_ref())
.get("run")
.and_then(|run| run.get("checkpoint"))
.expect("run.checkpoint")
}
fn run_hooks(settings: &SettingsLayer) -> &[fabro_types::settings::run::HookEntry] {
fn run_hooks(settings: &serde_json::Value) -> &[serde_json::Value] {
settings
.run
.as_ref()
.map_or(&[], |run| run.hooks.as_slice())
.get("run")
.and_then(|run| run.get("hooks"))
.and_then(serde_json::Value::as_array)
.expect("run.hooks")
}
fn run_agent_mcps(
settings: &SettingsLayer,
) -> &std::collections::HashMap<String, fabro_types::settings::run::McpEntryLayer> {
fn run_agent_mcps(settings: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
settings
.run
.as_ref()
.and_then(|run| run.agent.as_ref())
.map(|agent| &agent.mcps)
.get("run")
.and_then(|run| run.get("agent"))
.and_then(|agent| agent.get("mcps"))
.and_then(serde_json::Value::as_object)
.expect("run.agent.mcps")
}
fn auto_approve_enabled(settings: &SettingsLayer) -> bool {
resolve_run(settings).execution.approval == fabro_types::settings::run::ApprovalMode::Auto
fn auto_approve_enabled(settings: &serde_json::Value) -> bool {
settings
.get("run")
.and_then(|run| run.get("execution"))
.and_then(|execution| execution.get("approval"))
.and_then(serde_json::Value::as_str)
== Some("auto")
}
fn run_prepare_commands(settings: &SettingsLayer) -> Vec<String> {
resolve_run(settings).prepare.commands
fn run_prepare_commands(settings: &serde_json::Value) -> Vec<String> {
settings
.get("run")
.and_then(|run| run.get("prepare"))
.and_then(|prepare| prepare.get("commands"))
.and_then(serde_json::Value::as_array)
.expect("run.prepare.commands")
.iter()
.map(|value| {
value
.as_str()
.expect("command should be a string")
.to_string()
})
.collect()
}
fn server_storage_root(settings: &SettingsLayer) -> String {
resolve_server(settings).storage.root.as_source()
fn server_storage_root(settings: &serde_json::Value) -> &str {
settings
.get("server")
.and_then(|server| server.get("storage"))
.and_then(|storage| storage.get("root"))
.and_then(serde_json::Value::as_str)
.expect("server.storage.root")
}
fn server_settings_fixture() -> SettingsLayer {
fn server_settings_layer_fixture() -> SettingsLayer {
parse_settings_layer(
r#"
_version = 1
@ -148,7 +148,13 @@ shared = "server"
.expect("server settings fixture should parse")
}
fn server_settings_body(settings: &SettingsLayer) -> String {
fn resolved_server_settings_fixture() -> serde_json::Value {
let settings = fabro_config::resolve(&server_settings_layer_fixture())
.expect("server settings fixture should resolve");
serde_json::to_value(settings).expect("resolved settings payload should serialize")
}
fn server_settings_body(settings: &serde_json::Value) -> String {
serde_json::to_string(settings).expect("settings payload should serialize")
}
@ -383,42 +389,26 @@ fn settings_local_merges_cli_and_project_defaults() {
.clone();
let cfg = parse_settings(&output);
assert_eq!(
cfg.project
.as_ref()
.and_then(|project| project.directory.as_deref()),
Some(".")
);
assert_eq!(
cfg.workflow
.as_ref()
.and_then(|workflow| workflow.graph.as_deref()),
Some("workflow.fabro")
);
assert_eq!(
cfg.run
.as_ref()
.and_then(|run| run.execution.as_ref())
.and_then(|execution| execution.approval),
Some(fabro_types::settings::run::ApprovalMode::Prompt)
);
assert_eq!(
cfg.run
.as_ref()
.and_then(|run| run.sandbox.as_ref())
.and_then(|sandbox| sandbox.provider.as_deref()),
Some("daytona")
);
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
assert_eq!(cfg["run"]["sandbox"]["provider"].as_str(), Some("daytona"));
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("openai"));
assert_eq!(run_goal_inline(&cfg).as_deref(), None);
assert_eq!(resolve_project(&cfg).directory, ".");
// v2 R22: run.inputs replaces the inherited map wholesale rather than
// merging by key, so the project layer wipes out the CLI layer's inputs.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("project_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
assert_eq!(
vars.get("project_only").and_then(serde_json::Value::as_str),
Some("1")
);
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("project")
);
assert!(
!vars.contains_key("cli_only"),
"run.inputs should replace across layers, not merge by key"
@ -427,14 +417,13 @@ fn settings_local_merges_cli_and_project_defaults() {
// v2 R71: provider-native maps such as run.sandbox.daytona.labels remain
// sticky merge-by-key, so CLI labels persist under the project layer.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox.daytona.as_ref().expect("daytona").labels;
assert_eq!(labels.get("cli_only").map(String::as_str), Some("1"));
assert_eq!(labels.get("shared").map(String::as_str), Some("cli"));
let labels = &sandbox["daytona"]["labels"];
assert_eq!(labels["cli_only"].as_str(), Some("1"));
assert_eq!(labels["shared"].as_str(), Some("cli"));
}
#[test]
fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
use fabro_types::settings::run::McpEntryLayer;
let context = test_context!();
let project = setup_settings_fixture(&context);
@ -461,10 +450,10 @@ fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
// checkpoint.exclude_globs is a security/policy list: replace by default.
let checkpoint = run_checkpoint(&cfg);
assert_eq!(checkpoint.exclude_globs, vec![
"run-only".to_string(),
"shared".to_string()
]);
assert_eq!(
checkpoint["exclude_globs"],
serde_json::json!(["run-only", "shared"])
);
// Hooks: id-based replacement. The "shared" hook appears in both cli and
// workflow layers and resolves to the workflow entry; project and run-only
@ -473,62 +462,35 @@ fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
assert!(hooks.len() >= 2);
let shared_hook = hooks
.iter()
.find(|hook| hook.name.as_deref() == Some("shared"))
.find(|hook| hook["name"].as_str() == Some("shared"))
.expect("shared hook");
assert_eq!(
shared_hook
.script
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("echo run")
);
assert_eq!(shared_hook["command"].as_str(), Some("echo run"));
assert!(
hooks
.iter()
.any(|hook| hook.name.as_deref() == Some("run-only"))
.any(|hook| hook["name"].as_str() == Some("run-only"))
);
let mcps = run_agent_mcps(&cfg);
match mcps.get("shared").expect("shared mcp") {
McpEntryLayer::Stdio { command, .. } => {
let command = command.as_ref().expect("command");
let parts: Vec<String> = command
.iter()
.map(fabro_types::settings::InterpString::as_source)
.collect();
assert_eq!(parts, vec!["echo".to_string(), "run".to_string()]);
}
other => panic!("unexpected MCP transport: {other:?}"),
}
let shared = mcps.get("shared").expect("shared mcp");
assert_eq!(shared["transport"]["type"].as_str(), Some("stdio"));
assert_eq!(
shared["transport"]["command"],
serde_json::json!(["echo", "run"])
);
assert!(mcps.contains_key("run_only"));
// run.sandbox.daytona.labels stays sticky merge-by-key per R71.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox.daytona.as_ref().expect("daytona").labels;
assert_eq!(labels.get("run_only").map(String::as_str), Some("1"));
assert_eq!(labels.get("shared").map(String::as_str), Some("run"));
let labels = &sandbox["daytona"]["labels"];
assert_eq!(labels["run_only"].as_str(), Some("1"));
assert_eq!(labels["shared"].as_str(), Some("run"));
// run.sandbox.env stays sticky merge-by-key per R71.
let env = &sandbox.env;
assert_eq!(
env.get("CLI_ONLY")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("1")
);
assert_eq!(
env.get("RUN_ONLY")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("1")
);
assert_eq!(
env.get("SHARED")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("run")
);
let env = &sandbox["env"];
assert_eq!(env["CLI_ONLY"].as_str(), Some("1"));
assert_eq!(env["RUN_ONLY"].as_str(), Some("1"));
assert_eq!(env["SHARED"].as_str(), Some("run"));
}
#[test]
@ -557,7 +519,7 @@ fn settings_local_explicit_workflow_path_uses_workflow_project_layers() {
assert_eq!(run_prepare_commands(&cfg), vec![
"workflow-setup".to_string()
]);
assert_eq!(run_sandbox(&cfg).preserve, Some(true));
assert_eq!(run_sandbox(&cfg)["preserve"].as_bool(), Some(true));
}
#[test]
@ -706,15 +668,10 @@ name = "legacy-model"
.stderr(predicate::str::contains("Rename it to"));
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(
resolve_cli(&cfg).output.verbosity,
fabro_types::settings::cli::OutputVerbosity::Normal
);
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
assert!(
cfg.run
.as_ref()
.and_then(|run| run.model.as_ref())
.is_none()
cfg["run"]["model"].get("name").is_none(),
"resolved dense settings should omit an unset run.model.name"
);
}
@ -746,7 +703,10 @@ shared = "legacy"
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
let vars = run_inputs(&cfg);
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("project")
);
}
#[test]
@ -783,7 +743,8 @@ name = "from-fabro-home"
String::from_utf8_lossy(&output.stderr),
);
let cfg: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let cfg = parse_settings_json(&output.stdout);
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose"));
assert_eq!(
cfg["run"]["model"]["name"].as_str(),
@ -831,15 +792,34 @@ fn settings_rejects_local_and_server_combination() {
}
#[test]
fn settings_fetches_server_settings_and_merges_with_local_config() {
fn settings_rejects_workflow_without_local() {
let context = test_context!();
let project = setup_settings_fixture(&context);
context
.settings()
.current_dir(project.path())
.arg("demo")
.assert()
.failure()
.stderr(predicate::str::contains(
"WORKFLOW requires --local; use `fabro settings --local WORKFLOW`",
));
}
#[test]
fn settings_fetches_server_resolved_settings() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let server = MockServer::start();
let server_settings = server_settings_fixture();
let server_settings = resolved_server_settings_fixture();
let mock = server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
then.status(200)
.header("Content-Type", "application/json")
.header("X-Fabro-Settings-View", "resolved")
.body(server_settings_body(&server_settings));
});
context.write_home(
@ -878,42 +858,29 @@ shared = "cli"
mock.assert();
let cfg = parse_settings(&output);
assert_eq!(
cfg.project
.as_ref()
.and_then(|project| project.directory.as_deref()),
Some(".")
);
assert_eq!(
cfg.workflow
.as_ref()
.and_then(|workflow| workflow.graph.as_deref()),
Some("workflow.fabro")
);
assert_eq!(
cfg.run
.as_ref()
.and_then(|run| run.execution.as_ref())
.and_then(|execution| execution.approval),
Some(fabro_types::settings::run::ApprovalMode::Prompt)
);
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
assert!(cfg.get("_version").is_none());
assert_eq!(cfg["project"]["directory"].as_str(), Some("."));
assert_eq!(cfg["workflow"]["graph"].as_str(), Some("workflow.fabro"));
assert_eq!(cfg["run"]["execution"]["approval"].as_str(), Some("prompt"));
assert_eq!(run_model_name(&cfg).as_deref(), Some("server-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("openai"));
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
assert_eq!(
resolve_cli(&cfg).output.verbosity,
fabro_types::settings::cli::OutputVerbosity::Verbose
);
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("normal"));
// R22: run.inputs replaces wholesale across layers. Project is the
// highest-precedence layer that sets inputs, so project's vars win
// and server-side vars are discarded rather than merged.
// Server-backed mode now returns the selected server's own dense resolved
// settings; local project/user overlays are not merged into the output.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("project_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
assert_eq!(
vars.get("server_only").and_then(serde_json::Value::as_str),
Some("1")
);
assert_eq!(
vars.get("shared").and_then(serde_json::Value::as_str),
Some("server")
);
assert!(
!vars.contains_key("server_only"),
"v2 merge matrix replaces run.inputs wholesale; server_only should be dropped"
!vars.contains_key("project_only"),
"server-backed settings output must not include local workflow/project overlays"
);
}
@ -923,16 +890,21 @@ fn settings_cli_server_target_overrides_configured_server_target() {
let project = setup_settings_fixture(&context);
let configured_server = MockServer::start();
let configured_mock = configured_server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
then.status(500)
.body("configured-server-should-not-be-used");
});
let cli_server = MockServer::start();
let cli_server_settings = server_settings_fixture();
let cli_server_settings = resolved_server_settings_fixture();
let cli_mock = cli_server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
then.status(200)
.header("Content-Type", "application/json")
.header("X-Fabro-Settings-View", "resolved")
.body(server_settings_body(&cli_server_settings));
});
context.write_home(
@ -968,6 +940,46 @@ verbosity = "verbose"
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
}
#[test]
fn settings_errors_when_server_lacks_resolved_view_marker() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let server = MockServer::start();
let server_settings = resolved_server_settings_fixture();
let mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/settings")
.query_param("view", "resolved");
then.status(200)
.header("Content-Type", "application/json")
.body(server_settings_body(&server_settings));
});
context.write_home(
".fabro/settings.toml",
format!(
r#"
_version = 1
[cli.target]
type = "http"
url = "{}/api/v1"
"#,
server.base_url()
),
);
context
.settings()
.current_dir(project.path())
.assert()
.failure()
.stderr(predicate::str::contains(
"server does not support resolved settings view; upgrade the server or use --local",
));
mock.assert();
}
#[test]
fn settings_unreachable_http_target_fails_clearly() {
let context = test_context!();

View file

@ -48,8 +48,9 @@ impl EffectiveSettingsLayers {
}
}
/// Resolve layered configuration down to a single effective [`SettingsLayer`].
pub fn resolve_settings(
/// Materialize layered configuration down to a single effective
/// [`SettingsLayer`].
pub fn materialize_settings_layer(
layers: EffectiveSettingsLayers,
server_settings: Option<&SettingsLayer>,
mode: EffectiveSettingsMode,
@ -187,7 +188,7 @@ mod tests {
use fabro_types::settings::server::{ServerLayer, ServerSchedulerLayer, ServerStorageLayer};
use fabro_types::settings::{InterpString, SettingsLayer};
use super::{EffectiveSettingsLayers, EffectiveSettingsMode, resolve_settings};
use super::{EffectiveSettingsLayers, EffectiveSettingsMode, materialize_settings_layer};
use crate::parse::parse_settings_layer;
fn layer(source: &str) -> SettingsLayer {
@ -196,7 +197,7 @@ mod tests {
#[test]
fn local_only_merges_project_and_user_layers() {
let settings = resolve_settings(
let settings = materialize_settings_layer(
EffectiveSettingsLayers::new(
SettingsLayer::default(),
SettingsLayer::default(),
@ -286,7 +287,7 @@ shared = "user"
#[test]
fn local_only_merges_workflow_project_user() {
let settings = resolve_settings(
let settings = materialize_settings_layer(
EffectiveSettingsLayers::new(
SettingsLayer::default(),
layer(
@ -381,7 +382,7 @@ root = "/tmp/should-be-inert"
"#,
);
let settings = resolve_settings(
let settings = materialize_settings_layer(
EffectiveSettingsLayers::new(
SettingsLayer::default(),
SettingsLayer::default(),
@ -445,7 +446,7 @@ root = "/tmp/should-be-inert"
..SettingsLayer::default()
};
let settings = resolve_settings(
let settings = materialize_settings_layer(
EffectiveSettingsLayers::default(),
Some(&server_settings),
EffectiveSettingsMode::LocalDaemon,

View file

@ -41,7 +41,7 @@ pub fn load_and_resolve(
server_settings: Option<&SettingsLayer>,
mode: effective_settings::EffectiveSettingsMode,
) -> Result<Settings> {
let layer = effective_settings::resolve_settings(layers, server_settings, mode)?;
let layer = effective_settings::materialize_settings_layer(layers, server_settings, mode)?;
resolve(&layer).map_err(|errors| Error::resolve("failed to resolve settings", errors))
}

View file

@ -110,3 +110,107 @@ pub(crate) fn parse_socket_addr(
pub(crate) fn default_interp(path: impl AsRef<std::path::Path>) -> InterpString {
InterpString::parse(&path.as_ref().to_string_lossy())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use fabro_types::settings::run::{HookType, McpTransport};
use super::resolve;
use crate::parse_settings_layer;
#[test]
fn resolve_preserves_source_templates_for_mcp_and_hook_strings() {
let settings = parse_settings_layer(
r#"
_version = 1
[run.agent.mcps.stdio]
type = "stdio"
command = ["fabro-mcp", "--stdio"]
[run.agent.mcps.stdio.env]
TOKEN = "Bearer {{ env.MCP_STDIO_TOKEN }}"
[run.agent.mcps.http]
type = "http"
url = "https://mcp.example.com"
[run.agent.mcps.http.headers]
Authorization = "Bearer {{ env.MCP_HTTP_TOKEN }}"
[run.agent.mcps.sandbox]
type = "sandbox"
command = ["fabro-mcp", "--sandbox"]
port = 3333
[run.agent.mcps.sandbox.env]
TOKEN = "{{ env.MCP_SANDBOX_TOKEN }}"
[[run.hooks]]
name = "notify"
event = "run_complete"
url = "https://hooks.example.com"
[run.hooks.headers]
Authorization = "Bearer {{ env.HOOK_TOKEN }}"
"#,
)
.expect("settings fixture should parse");
let resolved = resolve(&settings).expect("settings should resolve");
let mcps = &resolved.run.agent.mcps;
assert_eq!(
mcps.get("stdio").map(|mcp| &mcp.transport),
Some(&McpTransport::Stdio {
command: vec!["fabro-mcp".to_string(), "--stdio".to_string()],
env: HashMap::from([(
"TOKEN".to_string(),
"Bearer {{ env.MCP_STDIO_TOKEN }}".to_string(),
)]),
})
);
assert_eq!(
mcps.get("http").map(|mcp| &mcp.transport),
Some(&McpTransport::Http {
url: "https://mcp.example.com".to_string(),
headers: HashMap::from([(
"Authorization".to_string(),
"Bearer {{ env.MCP_HTTP_TOKEN }}".to_string(),
)]),
})
);
assert_eq!(
mcps.get("sandbox").map(|mcp| &mcp.transport),
Some(&McpTransport::Sandbox {
command: vec!["fabro-mcp".to_string(), "--sandbox".to_string()],
port: 3333,
env: HashMap::from([(
"TOKEN".to_string(),
"{{ env.MCP_SANDBOX_TOKEN }}".to_string(),
)]),
})
);
let hook = resolved
.run
.hooks
.iter()
.find(|hook| hook.name.as_deref() == Some("notify"))
.expect("notify hook");
assert_eq!(
hook.resolved_hook_type().as_deref(),
Some(&HookType::Http {
url: "https://hooks.example.com".to_string(),
headers: Some(HashMap::from([(
"Authorization".to_string(),
"Bearer {{ env.HOOK_TOKEN }}".to_string(),
)])),
allowed_env_vars: Vec::new(),
tls: fabro_types::settings::run::TlsMode::Verify,
})
);
}
}

View file

@ -17,6 +17,7 @@ use serde_json::json;
use crate::error::ApiError;
use crate::jwt_auth::AuthenticatedService;
use crate::server::{AppState, PaginationParams};
use crate::settings_view;
fn paginated_response<T: serde::Serialize>(
items: Vec<T>,
@ -494,8 +495,22 @@ pub(crate) async fn list_query_history(
pub(crate) async fn get_server_settings(
_auth: AuthenticatedService,
State(_state): State<Arc<AppState>>,
Query(query): Query<settings_view::SettingsQuery>,
) -> Response {
(StatusCode::OK, Json(settings::server_settings())).into_response()
match query.view {
settings_view::SettingsApiView::Layer => {
(StatusCode::OK, Json(settings::server_settings())).into_response()
}
settings_view::SettingsApiView::Resolved => {
let mut response =
(StatusCode::OK, Json(settings::resolved_server_settings())).into_response();
response.headers_mut().insert(
settings_view::RESOLVED_VIEW_HEADER_NAME,
axum::http::HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE),
);
response
}
}
}
// ── System ────────────────────────────────────────────────────────────
@ -1539,4 +1554,149 @@ mod settings {
}
})
}
pub(super) fn resolved_server_settings() -> serde_json::Value {
serde_json::json!({
"project": {
"directory": "."
},
"workflow": {
"graph": "workflow.fabro"
},
"run": {
"model": {
"provider": "anthropic",
"name": "claude-sonnet",
"fallbacks": []
},
"execution": {
"mode": "normal",
"approval": "prompt",
"retros": true
},
"sandbox": {
"provider": "daytona",
"preserve": false,
"devcontainer": false,
"env": {},
"local": {
"worktree_mode": "clean"
},
"daytona": {
"auto_stop_interval": 60,
"labels": {},
"network": "block",
"skip_clone": false
}
},
"notifications": {},
"interviews": {},
"agent": {
"mcps": {}
},
"hooks": [],
"scm": {},
"artifacts": {
"include": []
},
"inputs": {},
"metadata": {},
"git": {},
"prepare": {
"commands": [],
"timeout_ms": 300000
},
"checkpoint": {
"exclude_globs": []
}
},
"cli": {
"auth": {},
"exec": {
"prevent_idle_sleep": false,
"model": {},
"agent": {
"mcps": {}
}
},
"output": {
"format": "text",
"verbosity": "normal"
},
"updates": {
"check": true
},
"logging": {}
},
"server": {
"api": {
"url": "https://api.fabro.example.com"
},
"web": {
"enabled": true,
"url": "https://fabro.example.com"
},
"auth": {
"api": {
"jwt": {
"enabled": true
}
},
"web": {
"allowed_usernames": ["brynary", "alice"],
"providers": {
"github": {
"enabled": true,
"client_id": "Iv1.abc123"
}
}
}
},
"storage": {
"root": "/home/fabro/.fabro"
},
"artifacts": {
"prefix": "",
"store": {
"type": "local",
"root": ""
}
},
"slatedb": {
"prefix": "",
"store": {
"type": "local",
"root": ""
},
"flush_interval": "0s"
},
"scheduler": {
"max_concurrent_runs": 10
},
"logging": {},
"integrations": {
"github": {
"enabled": false,
"strategy": "gh_cli",
"app_id": "12345",
"client_id": "Iv1.abc123",
"slug": "fabro-dev",
"permissions": {}
},
"slack": {
"enabled": false
},
"discord": {
"enabled": false
},
"teams": {
"enabled": false
}
}
},
"features": {
"session_sandboxes": false
}
})
}
}

View file

@ -84,7 +84,7 @@ pub(crate) fn prepare_manifest_with_mode(
.try_fold(SettingsLayer::default(), |layer, config| {
Ok::<_, anyhow::Error>(combine_files(layer, parse_manifest_config(config)?))
})?;
let mut settings = effective_settings::resolve_settings(
let mut settings = effective_settings::materialize_settings_layer(
EffectiveSettingsLayers::new(args_layer, workflow_layer, project_layer, user_layer),
Some(server_settings),
if local_daemon_mode {

View file

@ -1123,35 +1123,53 @@ async fn health() -> Response {
async fn get_server_settings(
_auth: AuthenticatedService,
State(state): State<Arc<AppState>>,
Query(query): Query<settings_view::SettingsQuery>,
) -> Response {
let settings = state.settings.read().unwrap().clone();
let redacted = settings_view::redact_for_api(&settings);
let mut value = match serde_json::to_value(&redacted) {
Ok(value) => value,
Err(err) => {
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
.into_response();
match query.view {
settings_view::SettingsApiView::Layer => {
let redacted = settings_view::redact_for_api(&settings);
let mut value = match serde_json::to_value(&redacted) {
Ok(value) => value,
Err(err) => {
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
.into_response();
}
};
strip_nulls(&mut value);
(StatusCode::OK, Json(value)).into_response()
}
};
strip_nulls(&mut value);
(StatusCode::OK, Json(value)).into_response()
settings_view::SettingsApiView::Resolved => {
let resolved = match fabro_config::resolve(&settings) {
Ok(settings) => settings,
Err(err) => {
return ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,
format!("failed to resolve settings: {err:?}"),
)
.into_response();
}
};
let mut value = match settings_view::redact_resolved_value(&resolved) {
Ok(value) => value,
Err(err) => {
return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
.into_response();
}
};
strip_nulls(&mut value);
let mut response = (StatusCode::OK, Json(value)).into_response();
response.headers_mut().insert(
settings_view::RESOLVED_VIEW_HEADER_NAME,
HeaderValue::from_static(settings_view::RESOLVED_VIEW_HEADER_VALUE),
);
response
}
}
}
fn strip_nulls(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for child in map.values_mut() {
strip_nulls(child);
}
map.retain(|_, child| !child.is_null());
}
serde_json::Value::Array(values) => {
for child in values {
strip_nulls(child);
}
}
_ => {}
}
settings_view::strip_nulls(value);
}
async fn get_system_info(
@ -6208,6 +6226,34 @@ mod tests {
format!("/api/v1{path}")
}
#[tokio::test]
async fn resolved_settings_view_returns_internal_error_when_runtime_settings_stop_resolving() {
let state = create_app_state();
*state.settings.write().unwrap() = fabro_config::parse_settings_layer(
r#"
_version = 1
[cli.target]
type = "http"
"#,
)
.expect("settings fixture should parse");
let app = build_router(state, AuthMode::Disabled);
let response = app
.oneshot(
Request::builder()
.method("GET")
.uri(api("/settings?view=resolved"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn create_secret_stores_file_secret_and_excludes_it_from_snapshot() {
let state = create_app_state();

View file

@ -34,7 +34,40 @@
//! Any future field that carries a raw secret in-band (without env
//! interpolation) must be added to the drop list below.
use fabro_types::settings::SettingsLayer;
use fabro_types::settings::{Settings, SettingsLayer};
use serde::Deserialize;
pub(crate) const RESOLVED_VIEW_HEADER_NAME: &str = "X-Fabro-Settings-View";
pub(crate) const RESOLVED_VIEW_HEADER_VALUE: &str = "resolved";
const REDACTED_PATHS: &[&[&str]] = &[
&["server", "listen"],
&["server", "auth", "api", "jwt", "issuer"],
&["server", "auth", "api", "jwt", "audience"],
&["server", "auth", "api", "mtls", "ca"],
&[
"server",
"auth",
"web",
"providers",
"github",
"client_secret",
],
];
#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum SettingsApiView {
#[default]
Layer,
Resolved,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub(crate) struct SettingsQuery {
#[serde(default)]
pub(crate) view: SettingsApiView,
}
/// Build a redacted clone of `settings` safe to serialize outward.
///
@ -70,6 +103,54 @@ pub(crate) fn redact_for_api(settings: &SettingsLayer) -> SettingsLayer {
out
}
pub(crate) fn redact_resolved_value(settings: &Settings) -> serde_json::Result<serde_json::Value> {
let mut value = serde_json::to_value(settings)?;
redact_value_paths(&mut value);
Ok(value)
}
pub(crate) fn strip_nulls(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for child in map.values_mut() {
strip_nulls(child);
}
map.retain(|_, child| !child.is_null());
}
serde_json::Value::Array(values) => {
for child in values {
strip_nulls(child);
}
}
_ => {}
}
}
fn redact_value_paths(value: &mut serde_json::Value) {
for path in REDACTED_PATHS {
remove_path(value, path);
}
}
fn remove_path(value: &mut serde_json::Value, path: &[&str]) {
let Some((head, tail)) = path.split_first() else {
return;
};
let Some(object) = value.as_object_mut() else {
return;
};
if tail.is_empty() {
object.remove(*head);
return;
}
if let Some(child) = object.get_mut(*head) {
remove_path(child, tail);
}
}
#[cfg(test)]
mod tests {
use fabro_config::parse_settings_layer;
@ -268,4 +349,78 @@ default_channel = "{{ env.SLACK_CHANNEL }}"
Some("{{ env.SLACK_CHANNEL }}".to_string())
);
}
#[test]
fn redacts_dense_resolved_settings_with_the_same_secret_paths() {
let settings = parse(
r#"
_version = 1
[server.listen]
type = "tcp"
address = "127.0.0.1:32276"
[server.listen.tls]
cert = "/etc/fabro/tls/cert.pem"
key = "/etc/fabro/tls/key.pem"
ca = "/etc/fabro/tls/ca.pem"
[server.auth.api.jwt]
enabled = true
issuer = "https://auth.example.com"
audience = "fabro"
[server.auth.api.mtls]
enabled = true
ca = "/etc/fabro/tls/ca.pem"
[server.auth.web.providers.github]
enabled = true
client_id = "Iv1.abcdef"
client_secret = "{{ env.GITHUB_OAUTH_SECRET }}"
[server.storage]
root = "{{ env.FABRO_STORAGE_ROOT }}"
"#,
);
let resolved = fabro_config::resolve(&settings).expect("settings should resolve");
let mut redacted =
redact_resolved_value(&resolved).expect("resolved settings should serialize");
assert!(redacted["server"].get("listen").is_none());
assert_eq!(redacted["server"]["auth"]["api"]["jwt"]["enabled"], true);
assert!(
redacted["server"]["auth"]["api"]["jwt"]
.get("issuer")
.is_none()
);
assert!(
redacted["server"]["auth"]["api"]["jwt"]
.get("audience")
.is_none()
);
assert_eq!(redacted["server"]["auth"]["api"]["mtls"]["enabled"], true);
assert!(
redacted["server"]["auth"]["api"]["mtls"]
.get("ca")
.is_none()
);
assert_eq!(
redacted["server"]["auth"]["web"]["providers"]["github"]["client_id"],
"Iv1.abcdef"
);
assert!(
redacted["server"]["auth"]["web"]["providers"]["github"]
.get("client_secret")
.is_none()
);
assert_eq!(
redacted["server"]["storage"]["root"],
"{{ env.FABRO_STORAGE_ROOT }}"
);
strip_nulls(&mut redacted);
assert!(redacted["server"].get("listen").is_none());
}
}

View file

@ -9,11 +9,20 @@ use tower::ServiceExt;
use crate::helpers::body_json;
#[tokio::test]
async fn retrieve_server_settings_returns_runtime_settings() {
async fn retrieve_server_settings_default_view_returns_redacted_layer_settings() {
let settings: SettingsLayer = parse_settings_layer(
r#"
_version = 1
[server.listen]
type = "tcp"
address = "127.0.0.1:32276"
[server.listen.tls]
cert = "/etc/fabro/tls/cert.pem"
key = "/etc/fabro/tls/key.pem"
ca = "/etc/fabro/tls/ca.pem"
[server.storage]
root = "/srv/fabro"
@ -23,6 +32,20 @@ max_concurrent_runs = 9
[cli.output]
verbosity = "verbose"
[server.auth.api.jwt]
enabled = true
issuer = "https://auth.example.com"
audience = "fabro"
[server.auth.api.mtls]
enabled = true
ca = "/etc/fabro/ca.pem"
[server.auth.web.providers.github]
enabled = true
client_id = "Iv1.abcdef"
client_secret = "{{ env.GITHUB_OAUTH_SECRET }}"
[run.inputs]
server_only = "1"
"#,
@ -42,10 +65,96 @@ server_only = "1"
assert_eq!(response.status(), StatusCode::OK);
let body = body_json(response.into_body()).await;
// `/api/v1/settings` emits the v2 SettingsLayer shape directly now.
// Stage 6.6 will replace this with an explicit allow-list DTO.
assert_eq!(body["_version"], 1);
assert_eq!(body["server"]["storage"]["root"], "/srv/fabro");
assert_eq!(body["server"]["scheduler"]["max_concurrent_runs"], 9);
assert_eq!(body["cli"]["output"]["verbosity"], "verbose");
assert_eq!(body["run"]["inputs"]["server_only"], "1");
assert!(body["server"].get("listen").is_none());
assert_eq!(body["server"]["auth"]["api"]["jwt"]["enabled"], true);
assert!(body["server"]["auth"]["api"]["jwt"].get("issuer").is_none());
assert!(
body["server"]["auth"]["api"]["jwt"]
.get("audience")
.is_none()
);
assert_eq!(body["server"]["auth"]["api"]["mtls"]["enabled"], true);
assert!(body["server"]["auth"]["api"]["mtls"].get("ca").is_none());
assert_eq!(
body["server"]["auth"]["web"]["providers"]["github"]["client_id"],
"Iv1.abcdef"
);
assert!(
body["server"]["auth"]["web"]["providers"]["github"]
.get("client_secret")
.is_none()
);
}
#[tokio::test]
async fn retrieve_server_settings_resolved_view_returns_dense_settings_and_marker() {
let settings: SettingsLayer = parse_settings_layer(
r#"
_version = 1
[server.listen]
type = "tcp"
address = "127.0.0.1:32276"
[server.storage]
root = "/srv/fabro"
[server.auth.web.providers.github]
enabled = true
client_id = "Iv1.abcdef"
client_secret = "{{ env.GITHUB_OAUTH_SECRET }}"
[run.model]
provider = "openai"
name = "server-model"
[run.inputs]
server_only = "1"
"#,
)
.expect("settings fixture should parse");
let app = build_router(
create_app_state_with_options(settings, 5),
AuthMode::Disabled,
);
let request = Request::builder()
.method("GET")
.uri("/api/v1/settings?view=resolved")
.body(Body::empty())
.unwrap();
let response = app.oneshot(request).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("x-fabro-settings-view")
.and_then(|value| value.to_str().ok()),
Some("resolved")
);
let body = body_json(response.into_body()).await;
assert!(body.get("_version").is_none());
assert_eq!(body["project"]["directory"], ".");
assert_eq!(body["workflow"]["graph"], "workflow.fabro");
assert_eq!(body["run"]["execution"]["approval"], "prompt");
assert_eq!(body["run"]["model"]["provider"], "openai");
assert_eq!(body["run"]["model"]["name"], "server-model");
assert_eq!(body["run"]["inputs"]["server_only"], "1");
assert_eq!(body["server"]["storage"]["root"], "/srv/fabro");
assert!(body["server"].get("listen").is_none());
assert_eq!(
body["server"]["auth"]["web"]["providers"]["github"]["client_id"],
"Iv1.abcdef"
);
assert!(
body["server"]["auth"]["web"]["providers"]["github"]
.get("client_secret")
.is_none()
);
}

View file

@ -13,7 +13,7 @@ use super::interp::InterpString;
use super::run::{AgentPermissions, McpEntryLayer, McpServerSettings};
/// A structurally resolved `[cli]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliSettings {
pub target: Option<CliTargetSettings>,
pub auth: CliAuthSettings,
@ -23,7 +23,8 @@ pub struct CliSettings {
pub logging: CliLoggingSettings,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CliTargetSettings {
Http {
url: InterpString,
@ -34,49 +35,49 @@ pub enum CliTargetSettings {
},
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CliTargetTlsSettings {
pub cert: InterpString,
pub key: InterpString,
pub ca: InterpString,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliAuthSettings {
pub strategy: Option<CliAuthStrategy>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliExecSettings {
pub prevent_idle_sleep: bool,
pub model: CliExecModelSettings,
pub agent: CliExecAgentSettings,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliExecModelSettings {
pub provider: Option<InterpString>,
pub name: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliExecAgentSettings {
pub permissions: Option<AgentPermissions>,
pub mcps: HashMap<String, McpServerSettings>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliOutputSettings {
pub format: OutputFormat,
pub verbosity: OutputVerbosity,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliUpdatesSettings {
pub check: bool,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct CliLoggingSettings {
pub level: Option<String>,
}

View file

@ -6,7 +6,7 @@
use serde::{Deserialize, Serialize};
/// A structurally resolved `[features]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct FeaturesSettings {
pub session_sandboxes: bool,
}

View file

@ -8,7 +8,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// A structurally resolved `[project]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ProjectSettings {
pub name: Option<String>,
pub description: Option<String>,

View file

@ -1,9 +1,11 @@
use serde::Serialize;
use super::{
CliSettings, FeaturesSettings, ProjectSettings, RunSettings, ServerSettings, WorkflowSettings,
};
/// A fully resolved settings view across all namespaces.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct Settings {
pub project: ProjectSettings,
pub workflow: WorkflowSettings,
@ -12,3 +14,160 @@ pub struct Settings {
pub server: ServerSettings,
pub features: FeaturesSettings,
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::time::Duration as StdDuration;
use serde_json::json;
use super::Settings;
use crate::settings::cli::{CliTargetSettings, CliTargetTlsSettings};
use crate::settings::interp::InterpString;
use crate::settings::run::{
DockerfileSource, McpServerSettings, McpTransport, RunAgentSettings, RunGoal, RunSettings,
};
use crate::settings::server::{
ObjectStoreSettings, ServerListenSettings, ServerSettings, ServerSlateDbSettings, TlsConfig,
};
#[test]
fn settings_serializes_successfully() {
serde_json::to_value(Settings::default()).expect("resolved settings should serialize");
}
#[test]
fn resolved_enums_use_human_readable_tagged_shapes() {
assert_eq!(
serde_json::to_value(CliTargetSettings::Http {
url: InterpString::parse("https://api.example.com"),
tls: Some(CliTargetTlsSettings {
cert: InterpString::parse("/tmp/client.crt"),
key: InterpString::parse("/tmp/client.key"),
ca: InterpString::parse("/tmp/ca.pem"),
}),
})
.unwrap(),
json!({
"type": "http",
"url": "https://api.example.com",
"tls": {
"cert": "/tmp/client.crt",
"key": "/tmp/client.key",
"ca": "/tmp/ca.pem"
}
})
);
assert_eq!(
serde_json::to_value(RunGoal::Inline(InterpString::parse("ship it"))).unwrap(),
json!({
"type": "inline",
"value": "ship it"
})
);
assert_eq!(
serde_json::to_value(McpTransport::Sandbox {
command: vec!["fabro-mcp".to_string(), "--serve".to_string()],
port: 3333,
env: HashMap::from([("TOKEN".to_string(), "{{ env.MCP_TOKEN }}".to_string())]),
})
.unwrap(),
json!({
"type": "sandbox",
"command": ["fabro-mcp", "--serve"],
"port": 3333,
"env": {
"TOKEN": "{{ env.MCP_TOKEN }}"
}
})
);
assert_eq!(
serde_json::to_value(DockerfileSource::Path {
path: "Dockerfile".to_string(),
})
.unwrap(),
json!({
"type": "path",
"path": "Dockerfile"
})
);
assert_eq!(
serde_json::to_value(ObjectStoreSettings::S3 {
bucket: InterpString::parse("fabro-artifacts"),
region: InterpString::parse("us-east-1"),
endpoint: Some(InterpString::parse("https://s3.example.com")),
path_style: true,
})
.unwrap(),
json!({
"type": "s3",
"bucket": "fabro-artifacts",
"region": "us-east-1",
"endpoint": "https://s3.example.com",
"path_style": true
})
);
}
#[test]
fn socket_addrs_and_std_durations_use_settings_strings() {
assert_eq!(
serde_json::to_value(ServerListenSettings::Tcp {
address: "127.0.0.1:8080".parse().unwrap(),
tls: Some(TlsConfig {
cert: InterpString::parse("/tmp/server.crt"),
key: InterpString::parse("/tmp/server.key"),
ca: InterpString::parse("/tmp/server-ca.pem"),
}),
})
.unwrap(),
json!({
"type": "tcp",
"address": "127.0.0.1:8080",
"tls": {
"cert": "/tmp/server.crt",
"key": "/tmp/server.key",
"ca": "/tmp/server-ca.pem"
}
})
);
let mut settings = Settings::default();
settings.server = ServerSettings {
slatedb: ServerSlateDbSettings {
prefix: InterpString::parse("slatedb/"),
store: ObjectStoreSettings::Local {
root: InterpString::parse("/srv/slatedb"),
},
flush_interval: StdDuration::from_secs(30),
},
..ServerSettings::default()
};
settings.run = RunSettings {
agent: RunAgentSettings {
mcps: HashMap::from([("sandboxed".to_string(), McpServerSettings {
name: "sandboxed".to_string(),
transport: McpTransport::Http {
url: "https://mcp.example.com".to_string(),
headers: HashMap::from([(
"Authorization".to_string(),
"Bearer {{ env.MCP_TOKEN }}".to_string(),
)]),
},
startup_timeout_secs: 15,
tool_timeout_secs: 90,
})]),
..RunAgentSettings::default()
},
..RunSettings::default()
};
let value = serde_json::to_value(settings).unwrap();
assert_eq!(value["server"]["slatedb"]["flush_interval"], "30s");
}
}

View file

@ -9,6 +9,7 @@
use std::collections::HashMap;
use std::time::Duration as StdDuration;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Serialize};
use super::duration::Duration;
@ -16,7 +17,7 @@ use super::interp::InterpString;
use super::model_ref::ModelRef;
/// A structurally resolved `[run]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunSettings {
pub goal: Option<RunGoal>,
pub working_dir: Option<InterpString>,
@ -38,31 +39,32 @@ pub struct RunSettings {
}
/// The resolved source of a run goal.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum RunGoal {
Inline(InterpString),
File(InterpString),
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunModelSettings {
pub provider: Option<InterpString>,
pub name: Option<InterpString>,
pub fallbacks: Vec<ModelRef>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunGitSettings {
pub author: Option<GitAuthorSettings>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct GitAuthorSettings {
pub name: Option<InterpString>,
pub email: Option<InterpString>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RunPrepareSettings {
pub commands: Vec<String>,
pub timeout_ms: u64,
@ -77,7 +79,7 @@ impl Default for RunPrepareSettings {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RunExecutionSettings {
pub mode: RunMode,
pub approval: ApprovalMode,
@ -94,12 +96,12 @@ impl Default for RunExecutionSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunCheckpointSettings {
pub exclude_globs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RunSandboxSettings {
pub provider: String,
pub preserve: bool,
@ -122,12 +124,12 @@ impl Default for RunSandboxSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct LocalSandboxSettings {
pub worktree_mode: WorktreeMode,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct DaytonaSettings {
pub auto_stop_interval: Option<i32>,
pub labels: HashMap<String, String>,
@ -142,7 +144,27 @@ pub enum DockerfileSource {
Path { path: String },
}
#[derive(Debug, Clone, PartialEq)]
impl Serialize for DockerfileSource {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("DockerfileSource", 2)?;
match self {
Self::Inline(value) => {
state.serialize_field("type", "inline")?;
state.serialize_field("value", value)?;
}
Self::Path { path } => {
state.serialize_field("type", "path")?;
state.serialize_field("path", path)?;
}
}
state.end()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct DaytonaSnapshotSettings {
pub name: String,
pub cpu: Option<i32>,
@ -151,7 +173,7 @@ pub struct DaytonaSnapshotSettings {
pub dockerfile: Option<DockerfileSource>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct NotificationRouteSettings {
pub enabled: bool,
pub provider: Option<String>,
@ -161,12 +183,12 @@ pub struct NotificationRouteSettings {
pub teams: Option<NotificationProviderSettings>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct NotificationProviderSettings {
pub channel: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunInterviewsSettings {
pub provider: Option<String>,
pub slack: Option<InterviewProviderSettings>,
@ -174,18 +196,18 @@ pub struct RunInterviewsSettings {
pub teams: Option<InterviewProviderSettings>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct InterviewProviderSettings {
pub channel: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunAgentSettings {
pub permissions: Option<AgentPermissions>,
pub mcps: HashMap<String, McpServerSettings>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct McpServerSettings {
pub name: String,
pub transport: McpTransport,
@ -219,7 +241,8 @@ impl McpServerSettings {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum McpTransport {
Stdio {
command: Vec<String>,
@ -348,7 +371,7 @@ impl HookDefinition {
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct RunScmSettings {
pub provider: Option<String>,
pub owner: Option<InterpString>,
@ -359,7 +382,17 @@ pub struct RunScmSettings {
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ScmGitHubSettings;
#[derive(Debug, Clone, PartialEq)]
impl Serialize for ScmGitHubSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let state = serializer.serialize_struct("ScmGitHubSettings", 0)?;
state.end()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PullRequestSettings {
pub enabled: bool,
pub draft: bool,
@ -378,7 +411,7 @@ impl Default for PullRequestSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ArtifactsSettings {
pub include: Vec<String>,
}

View file

@ -9,13 +9,13 @@ use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::Duration as StdDuration;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize, Serializer};
use super::duration::Duration as DurationLayer;
use super::interp::InterpString;
/// A structurally resolved `[server]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerSettings {
pub listen: ServerListenSettings,
pub api: ServerApiSettings,
@ -29,9 +29,11 @@ pub struct ServerSettings {
pub integrations: ServerIntegrationsSettings,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ServerListenSettings {
Tcp {
#[serde(serialize_with = "serialize_socket_addr")]
address: SocketAddr,
tls: Option<TlsConfig>,
},
@ -48,7 +50,7 @@ impl Default for ServerListenSettings {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct TlsConfig {
pub cert: InterpString,
pub key: InterpString,
@ -65,12 +67,12 @@ impl Default for TlsConfig {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerApiSettings {
pub url: Option<InterpString>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerWebSettings {
pub enabled: bool,
pub url: InterpString,
@ -85,50 +87,50 @@ impl Default for ServerWebSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthSettings {
pub api: ServerAuthApiSettings,
pub web: ServerAuthWebSettings,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthApiSettings {
pub jwt: Option<ServerAuthApiJwtSettings>,
pub mtls: Option<ServerAuthApiMtlsSettings>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthApiJwtSettings {
pub enabled: bool,
pub issuer: Option<InterpString>,
pub audience: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthApiMtlsSettings {
pub enabled: bool,
pub ca: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthWebSettings {
pub allowed_usernames: Vec<String>,
pub providers: ServerAuthWebProvidersSettings,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerAuthWebProvidersSettings {
pub github: Option<GithubOauthSettings>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct GithubOauthSettings {
pub enabled: bool,
pub client_id: Option<InterpString>,
pub client_secret: Option<InterpString>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerStorageSettings {
pub root: InterpString,
}
@ -141,7 +143,7 @@ impl Default for ServerStorageSettings {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerArtifactsSettings {
pub prefix: InterpString,
pub store: ObjectStoreSettings,
@ -156,10 +158,11 @@ impl Default for ServerArtifactsSettings {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerSlateDbSettings {
pub prefix: InterpString,
pub store: ObjectStoreSettings,
#[serde(serialize_with = "serialize_std_duration")]
pub flush_interval: StdDuration,
}
@ -173,7 +176,8 @@ impl Default for ServerSlateDbSettings {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ObjectStoreSettings {
Local {
root: InterpString,
@ -194,17 +198,17 @@ impl Default for ObjectStoreSettings {
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerSchedulerSettings {
pub max_concurrent_runs: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerLoggingSettings {
pub level: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ServerIntegrationsSettings {
pub github: GithubIntegrationSettings,
pub slack: SlackIntegrationSettings,
@ -212,7 +216,7 @@ pub struct ServerIntegrationsSettings {
pub teams: TeamsIntegrationSettings,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct GithubIntegrationSettings {
pub enabled: bool,
pub strategy: GithubIntegrationStrategy,
@ -223,27 +227,41 @@ pub struct GithubIntegrationSettings {
pub webhooks: Option<IntegrationWebhooksSettings>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct SlackIntegrationSettings {
pub enabled: bool,
pub default_channel: Option<InterpString>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct DiscordIntegrationSettings {
pub enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct TeamsIntegrationSettings {
pub enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct IntegrationWebhooksSettings {
pub strategy: Option<WebhookStrategy>,
}
fn serialize_socket_addr<S>(value: &SocketAddr, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&value.to_string())
}
fn serialize_std_duration<S>(value: &StdDuration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&DurationLayer::from_std(*value).to_string())
}
/// A sparse `[server]` layer as it appears in a single settings file.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]

View file

@ -8,7 +8,7 @@ use std::collections::HashMap;
use serde::{Deserialize, Serialize};
/// A structurally resolved `[workflow]` view for consumers.
#[derive(Debug, Clone, Default, PartialEq)]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct WorkflowSettings {
pub name: Option<String>,
pub description: Option<String>,

View file

@ -37,8 +37,8 @@ import type { SecretMetadata } from '../models';
export const SecretsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @summary Store or update a secret
* Stores a secret in the workflow-visible vault. Anything stored here may be used by workflows.
* @summary Store or update a vault secret
* @param {CreateSecretRequest} createSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -80,7 +80,7 @@ export const SecretsApiAxiosParamCreator = function (configuration?: Configurati
},
/**
*
* @summary Delete a stored secret
* @summary Delete a vault secret
* @param {DeleteSecretRequest} deleteSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -121,8 +121,8 @@ export const SecretsApiAxiosParamCreator = function (configuration?: Configurati
};
},
/**
* Returns stored secret names and timestamps. Secret values are never exposed.
* @summary List stored secrets
* Returns workflow-visible vault secret names and timestamps. Secret values are never exposed.
* @summary List vault secrets
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -167,8 +167,8 @@ export const SecretsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SecretsApiAxiosParamCreator(configuration)
return {
/**
*
* @summary Store or update a secret
* Stores a secret in the workflow-visible vault. Anything stored here may be used by workflows.
* @summary Store or update a vault secret
* @param {CreateSecretRequest} createSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -181,7 +181,7 @@ export const SecretsApiFp = function(configuration?: Configuration) {
},
/**
*
* @summary Delete a stored secret
* @summary Delete a vault secret
* @param {DeleteSecretRequest} deleteSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -193,8 +193,8 @@ export const SecretsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Returns stored secret names and timestamps. Secret values are never exposed.
* @summary List stored secrets
* Returns workflow-visible vault secret names and timestamps. Secret values are never exposed.
* @summary List vault secrets
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -214,8 +214,8 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
const localVarFp = SecretsApiFp(configuration)
return {
/**
*
* @summary Store or update a secret
* Stores a secret in the workflow-visible vault. Anything stored here may be used by workflows.
* @summary Store or update a vault secret
* @param {CreateSecretRequest} createSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -225,7 +225,7 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
},
/**
*
* @summary Delete a stored secret
* @summary Delete a vault secret
* @param {DeleteSecretRequest} deleteSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -234,8 +234,8 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
return localVarFp.deleteSecretByName(deleteSecretRequest, options).then((request) => request(axios, basePath));
},
/**
* Returns stored secret names and timestamps. Secret values are never exposed.
* @summary List stored secrets
* Returns workflow-visible vault secret names and timestamps. Secret values are never exposed.
* @summary List vault secrets
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
@ -250,8 +250,8 @@ export const SecretsApiFactory = function (configuration?: Configuration, basePa
*/
export class SecretsApi extends BaseAPI {
/**
*
* @summary Store or update a secret
* Stores a secret in the workflow-visible vault. Anything stored here may be used by workflows.
* @summary Store or update a vault secret
* @param {CreateSecretRequest} createSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -262,7 +262,7 @@ export class SecretsApi extends BaseAPI {
/**
*
* @summary Delete a stored secret
* @summary Delete a vault secret
* @param {DeleteSecretRequest} deleteSecretRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@ -272,8 +272,8 @@ export class SecretsApi extends BaseAPI {
}
/**
* Returns stored secret names and timestamps. Secret values are never exposed.
* @summary List stored secrets
* Returns workflow-visible vault secret names and timestamps. Secret values are never exposed.
* @summary List vault secrets
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/

View file

@ -27,12 +27,13 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
export const SettingsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Returns the structured server settings.
* Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy.
* @summary Retrieve Server Settings
* @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveServerSettings: async (options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
retrieveServerSettings: async (view?: RetrieveServerSettingsViewEnum, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/api/v1/settings`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -52,6 +53,10 @@ export const SettingsApiAxiosParamCreator = function (configuration?: Configurat
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (view !== undefined) {
localVarQueryParameter['view'] = view;
}
localVarHeaderParameter['Accept'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -73,13 +78,14 @@ export const SettingsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SettingsApiAxiosParamCreator(configuration)
return {
/**
* Returns the structured server settings.
* Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy.
* @summary Retrieve Server Settings
* @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async retrieveServerSettings(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{ [key: string]: any; }>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(options);
async retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{ [key: string]: any; }>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveServerSettings(view, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SettingsApi.retrieveServerSettings']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
@ -94,13 +100,14 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP
const localVarFp = SettingsApiFp(configuration)
return {
/**
* Returns the structured server settings.
* Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy.
* @summary Retrieve Server Settings
* @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveServerSettings(options?: RawAxiosRequestConfig): AxiosPromise<{ [key: string]: any; }> {
return localVarFp.retrieveServerSettings(options).then((request) => request(axios, basePath));
retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig): AxiosPromise<{ [key: string]: any; }> {
return localVarFp.retrieveServerSettings(view, options).then((request) => request(axios, basePath));
},
};
};
@ -110,13 +117,19 @@ export const SettingsApiFactory = function (configuration?: Configuration, baseP
*/
export class SettingsApi extends BaseAPI {
/**
* Returns the structured server settings.
* Returns the server settings view selected by the optional `view` query parameter. `view=layer` (the default) returns the current sparse redacted `SettingsLayer` payload. `view=resolved` returns the server\'s dense resolved settings payload after applying the same redaction policy.
* @summary Retrieve Server Settings
* @param {RetrieveServerSettingsViewEnum} [view] Selects the server settings representation to return.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public retrieveServerSettings(options?: RawAxiosRequestConfig) {
return SettingsApiFp(this.configuration).retrieveServerSettings(options).then((request) => request(this.axios, this.basePath));
public retrieveServerSettings(view?: RetrieveServerSettingsViewEnum, options?: RawAxiosRequestConfig) {
return SettingsApiFp(this.configuration).retrieveServerSettings(view, options).then((request) => request(this.axios, this.basePath));
}
}
export const RetrieveServerSettingsViewEnum = {
LAYER: 'layer',
RESOLVED: 'resolved'
} as const;
export type RetrieveServerSettingsViewEnum = typeof RetrieveServerSettingsViewEnum[keyof typeof RetrieveServerSettingsViewEnum];