Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions crates/cli/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ struct FileGatewayConfig {
}

#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
struct FileUpstreamConfig {
openai_base_url: Option<String>,
openai_auth_header: Option<String>,
Expand Down Expand Up @@ -1214,16 +1215,25 @@ pub(crate) fn user_config_dir() -> Option<PathBuf> {
// Applies the typed TOML config model to the resolved runtime config. Missing sections and fields
// are ignored, preserving defaults and prior merge layers.
fn apply_file_config(resolved: &mut ResolvedConfig, value: toml::Value) -> Result<(), CliError> {
let config: FileConfig = value.try_into().map_err(|error| {
CliError::Config(format!("invalid gateway configuration shape: {error}"))
})?;
let config = parse_file_config(value)?;
apply_file_gateway_config(&mut resolved.gateway, config.gateway)?;
apply_file_upstream_config(&mut resolved.gateway, config.upstream)?;
apply_file_agents_config(&mut resolved.agents, config.agents);
logging::apply_file_logging_config(&mut resolved.logging, config.logging)?;
Ok(())
}

pub(crate) fn validate_shared_config_shape(value: toml::Value) -> Result<(), CliError> {
let _ = parse_file_config(value)?;
Ok(())
}

fn parse_file_config(value: toml::Value) -> Result<FileConfig, CliError> {
value
.try_into()
.map_err(|error| CliError::Config(format!("invalid gateway configuration shape: {error}")))
}

fn apply_file_gateway_config(
gateway: &mut GatewayConfig,
config: Option<FileGatewayConfig>,
Expand Down
36 changes: 35 additions & 1 deletion crates/cli/src/diagnostics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,40 @@ fn dynamic_plugin_host_config_check(plugin: &DynamicPluginReferenceInfo) -> Chec
}

fn layer_status(path: &Path) -> ConfigLayer {
let mut layer = toml_layer_status(path);
if !matches!(layer.status, Status::Pass) {
return layer;
}
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(err) => {
layer.status = Status::Fail;
layer.active = false;
layer.details = format!("unreadable: {err}");
return layer;
}
};
let table = match text.parse::<toml::Table>() {
Ok(table) => table,
Err(err) => {
layer.status = Status::Fail;
layer.active = false;
layer.details = format!("invalid TOML: {err}");
return layer;
}
};
match crate::configuration::validate_shared_config_shape(toml::Value::Table(table)) {
Ok(()) => layer,
Err(err) => ConfigLayer {
path: path.to_path_buf(),
status: Status::Fail,
active: false,
details: err.to_string(),
},
}
}

fn toml_layer_status(path: &Path) -> ConfigLayer {
if !path.exists() {
return ConfigLayer {
path: path.to_path_buf(),
Expand Down Expand Up @@ -324,7 +358,7 @@ fn plugin_layer_status(
contributing_paths: &[PathBuf],
plugin_error: Option<&str>,
) -> ConfigLayer {
let mut layer = layer_status(path);
let mut layer = toml_layer_status(path);
Comment thread
mnajafian-nv marked this conversation as resolved.
if let Some(error) = plugin_error.filter(|error| error.contains(&path.display().to_string()))
&& matches!(layer.status, Status::Pass)
{
Expand Down
32 changes: 32 additions & 0 deletions crates/cli/tests/coverage/shared/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3429,6 +3429,38 @@ fn malformed_shared_config_reports_context() {

assert!(error.contains("invalid gateway configuration shape"));

let invalid_upstream_key = temp.path().join("invalid-upstream-key.toml");
std::fs::write(
&invalid_upstream_key,
"[upstream]\nopenai_baseurl = \"https://example.test/v1\"\n",
)
.unwrap();
let args = GatewayOverrides {
config: Some(invalid_upstream_key),
..GatewayOverrides::default()
};

let error = resolve_server_config(&args).unwrap_err().to_string();

assert!(error.contains("invalid gateway configuration shape"));
assert!(error.contains("openai_baseurl"));

let invalid_nested_upstream = temp.path().join("invalid-nested-upstream.toml");
std::fs::write(
&invalid_nested_upstream,
"[upstream.openai]\nbase_url = \"https://example.test/v1\"\n",
)
.unwrap();
let args = GatewayOverrides {
config: Some(invalid_nested_upstream),
..GatewayOverrides::default()
};

let error = resolve_server_config(&args).unwrap_err().to_string();

assert!(error.contains("invalid gateway configuration shape"));
assert!(error.contains("openai"));

let plugin_config = temp.path().join("config-with-invalid-plugins.toml");
std::fs::write(&plugin_config, "").unwrap();
std::fs::write(temp.path().join("plugins.toml"), "version = [").unwrap();
Expand Down
16 changes: 16 additions & 0 deletions crates/cli/tests/coverage/shared/doctor_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,22 @@ fn layer_status_reports_missing_valid_invalid_and_non_directory_paths() {
assert_eq!(valid_layer.status, Status::Pass);
assert!(valid_layer.active);

let invalid_shape = temp.path().join("invalid-shape.toml");
std::fs::write(
&invalid_shape,
"[upstream.openai]\nbase_url = \"http://local\"\n",
)
.unwrap();
let invalid_shape_layer = layer_status(&invalid_shape);
assert_eq!(invalid_shape_layer.status, Status::Fail);
assert!(!invalid_shape_layer.active);
assert!(
invalid_shape_layer
.details
.contains("invalid gateway configuration shape")
);
assert!(invalid_shape_layer.details.contains("openai"));
Comment thread
mnajafian-nv marked this conversation as resolved.

let invalid = temp.path().join("invalid.toml");
std::fs::write(&invalid, "[upstream\n").unwrap();
let invalid_layer = layer_status(&invalid);
Expand Down
Loading