use serde::Deserialize;
use super::helpers::json_err;
use crate::core::config::{CompressionLevel, Config, TerseAgent};
use crate::core::tool_profiles::ToolProfile;
const COMPRESSION_OPTIONS: &[&str] = &["off", "lite", "standard", "max"];
const TERSE_OPTIONS: &[&str] = &["off", "lite", "full", "ultra"];
const TOOL_PROFILE_OPTIONS: &[&str] = &["minimal", "standard", "power", "lean"];
pub(super) fn handle(
path: &str,
_query_str: &str,
method: &str,
body: &str,
) -> Option<(&'static str, &'static str, String)> {
match path {
"/api/settings" if method.eq_ignore_ascii_case("POST") => Some(post_settings(body)),
"/api/settings" => Some(("200 OK", "application/json", settings_payload())),
_ => None,
}
}
#[derive(Deserialize)]
struct SettingReq {
key: String,
value: serde_json::Value,
}
fn post_settings(body: &str) -> (&'static str, &'static str, String) {
let req: SettingReq = match serde_json::from_str(body) {
Ok(r) => r,
Err(e) => {
return (
"400 Bad Request",
"application/json",
json_err(&format!("invalid JSON: {e}")),
);
}
};
let Some(value) = normalize_value(&req.value) else {
return (
"400 Bad Request",
"application/json",
json_err("value must be a string or boolean"),
);
};
match apply_setting(&req.key, &value) {
Ok(()) => ("200 OK", "application/json", settings_payload()),
Err(e) => ("400 Bad Request", "application/json", json_err(&e)),
}
}
fn normalize_value(v: &serde_json::Value) -> Option<String> {
match v {
serde_json::Value::String(s) => Some(s.trim().to_string()),
serde_json::Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
fn validate_setting(key: &str, value: &str) -> Result<(), String> {
match key {
"compression_level" => ensure_in(value, COMPRESSION_OPTIONS),
"terse_agent" => ensure_in(value, TERSE_OPTIONS),
"tool_profile" => ensure_in(value, TOOL_PROFILE_OPTIONS),
"structure_first" => ensure_bool(value),
_ => Err(format!("unknown or non-editable setting: '{key}'")),
}
}
fn apply_setting(key: &str, value: &str) -> Result<(), String> {
validate_setting(key, value)?;
match key {
"compression_level" => apply_compression(value),
"tool_profile" => apply_tool_profile(value),
"structure_first" => {
crate::core::config::setter::set_by_key("structure_first", value).map(|_| ())
}
"terse_agent" => apply_terse_agent(value),
_ => Err(format!("unknown or non-editable setting: '{key}'")),
}
}
fn apply_terse_agent(value: &str) -> Result<(), String> {
crate::core::config::setter::set_by_key("terse_agent", value)?;
let cfg = Config::load();
let _ = crate::core::terse::rules_inject::inject(&CompressionLevel::effective(&cfg));
Ok(())
}
fn apply_compression(value: &str) -> Result<(), String> {
let level = CompressionLevel::from_str_label(value)
.ok_or_else(|| format!("invalid compression level '{value}'"))?;
let cfg = Config::update_global(move |c| c.compression_level = level)
.map_err(|e| format!("Error saving config: {e}"))?;
let _ = crate::core::terse::rules_inject::inject(&cfg.compression_level);
Ok(())
}
fn apply_tool_profile(value: &str) -> Result<(), String> {
match value {
"minimal" | "standard" | "power" => {
crate::core::tool_profiles::set_profile_in_config(value)
}
"lean" => crate::core::tool_profiles::clear_profile_in_config(),
other => Err(format!("invalid tool profile '{other}'")),
}
}
fn ensure_in(value: &str, allowed: &[&str]) -> Result<(), String> {
if allowed.contains(&value) {
Ok(())
} else {
Err(format!(
"invalid value '{value}'. Allowed: {}",
allowed.join(", ")
))
}
}
fn ensure_bool(value: &str) -> Result<(), String> {
match value {
"true" | "false" | "1" | "0" | "yes" | "no" | "on" | "off" => Ok(()),
_ => Err(format!("expected a boolean, got '{value}'")),
}
}
fn settings_payload() -> String {
let cfg = Config::load();
let prov = Config::provenance();
let local = |key: &str| prov.local_overrides(key);
let payload = serde_json::json!({
"config_path": prov.config_path.as_ref().map(|p| p.display().to_string()),
"config_exists": prov.config_exists,
"parse_error": prov.parse_error,
"settings": {
"compression_level": {
"value": compression_canon(&CompressionLevel::effective(&cfg)),
"options": COMPRESSION_OPTIONS,
"env_override": env_present("LEAN_CTX_COMPRESSION"),
"local_override": local("compression_level"),
},
"tool_profile": {
"value": tool_profile_value(&cfg),
"options": TOOL_PROFILE_OPTIONS,
"env_override": env_present("LEAN_CTX_TOOL_PROFILE"),
"local_override": local("tool_profile"),
},
"structure_first": {
"value": cfg.structure_first_effective(),
"env_override": env_present("LEAN_CTX_STRUCTURE_FIRST"),
},
"terse_agent": {
"value": terse_canon(&cfg.terse_agent),
"options": TERSE_OPTIONS,
"env_override": env_present("LEAN_CTX_TERSE_AGENT"),
"local_override": local("terse_agent"),
},
}
});
payload.to_string()
}
fn env_present(key: &str) -> bool {
std::env::var_os(key).is_some_and(|v| !v.is_empty())
}
fn compression_canon(level: &CompressionLevel) -> &'static str {
match level {
CompressionLevel::Off => "off",
CompressionLevel::Lite => "lite",
CompressionLevel::Standard => "standard",
CompressionLevel::Max => "max",
}
}
fn terse_canon(t: &TerseAgent) -> &'static str {
match t {
TerseAgent::Off => "off",
TerseAgent::Lite => "lite",
TerseAgent::Full => "full",
TerseAgent::Ultra => "ultra",
}
}
fn tool_profile_canon(p: &ToolProfile) -> &'static str {
match p {
ToolProfile::Minimal => "minimal",
ToolProfile::Standard => "standard",
ToolProfile::Power => "power",
ToolProfile::Custom(_) => "custom",
}
}
fn tool_profile_value(cfg: &Config) -> &'static str {
if env_present("LEAN_CTX_TOOL_PROFILE") {
return tool_profile_canon(&cfg.tool_profile_effective());
}
if let Some(name) = cfg.tool_profile.as_deref()
&& let Some(profile) = ToolProfile::parse(name)
{
return tool_profile_canon(&profile);
}
if !cfg.tools_enabled.is_empty() {
return "custom";
}
"lean"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_accepts_known_enum_values() {
assert!(validate_setting("compression_level", "max").is_ok());
assert!(validate_setting("terse_agent", "ultra").is_ok());
assert!(validate_setting("tool_profile", "power").is_ok());
assert!(validate_setting("tool_profile", "lean").is_ok());
assert!(validate_setting("structure_first", "true").is_ok());
assert!(validate_setting("structure_first", "false").is_ok());
}
#[test]
fn validate_rejects_bad_values() {
assert!(validate_setting("compression_level", "ultra").is_err());
assert!(validate_setting("terse_agent", "max").is_err());
assert!(validate_setting("tool_profile", "turbo").is_err());
assert!(validate_setting("structure_first", "maybe").is_err());
}
#[test]
fn validate_rejects_unknown_or_dangerous_keys() {
assert!(validate_setting("proxy.anthropic_upstream", "http://evil").is_err());
assert!(validate_setting("allow_paths", "/etc").is_err());
assert!(validate_setting("", "x").is_err());
}
#[test]
fn normalize_bool_and_string_values() {
assert_eq!(
normalize_value(&serde_json::json!(true)).as_deref(),
Some("true")
);
assert_eq!(
normalize_value(&serde_json::json!(false)).as_deref(),
Some("false")
);
assert_eq!(
normalize_value(&serde_json::json!("max")).as_deref(),
Some("max")
);
assert_eq!(
normalize_value(&serde_json::json!(" power ")).as_deref(),
Some("power")
);
assert!(normalize_value(&serde_json::json!(42)).is_none());
assert!(normalize_value(&serde_json::json!(null)).is_none());
}
#[test]
fn tool_profile_value_distinguishes_lean_from_power() {
crate::test_env::remove_var("LEAN_CTX_TOOL_PROFILE");
let unpinned = Config {
tool_profile: None,
tools_enabled: vec![],
..Default::default()
};
assert_eq!(tool_profile_value(&unpinned), "lean");
let aliased = Config {
tool_profile: Some("lean".into()),
..Default::default()
};
assert_eq!(tool_profile_value(&aliased), "lean");
let pinned = Config {
tool_profile: Some("power".into()),
..Default::default()
};
assert_eq!(tool_profile_value(&pinned), "power");
let minimal = Config {
tool_profile: Some("minimal".into()),
..Default::default()
};
assert_eq!(tool_profile_value(&minimal), "minimal");
let custom = Config {
tool_profile: None,
tools_enabled: vec!["ctx_read".into()],
..Default::default()
};
assert_eq!(tool_profile_value(&custom), "custom");
}
#[test]
fn payload_is_valid_json_with_all_four_settings() {
let raw = settings_payload();
let v: serde_json::Value = serde_json::from_str(&raw).expect("valid JSON");
let s = &v["settings"];
for key in [
"compression_level",
"tool_profile",
"structure_first",
"terse_agent",
] {
assert!(s.get(key).is_some(), "missing setting {key}");
assert!(
s[key].get("env_override").is_some(),
"missing env_override for {key}"
);
}
}
#[test]
fn payload_exposes_config_provenance() {
let raw = settings_payload();
let v: serde_json::Value = serde_json::from_str(&raw).expect("valid JSON");
assert!(v.get("config_path").is_some(), "missing config_path");
assert!(
v.get("config_exists")
.is_some_and(serde_json::Value::is_boolean),
"config_exists must be a bool"
);
assert!(v.get("parse_error").is_some(), "missing parse_error key");
let s = &v["settings"];
for key in ["compression_level", "tool_profile", "terse_agent"] {
assert!(
s[key]
.get("local_override")
.is_some_and(serde_json::Value::is_boolean),
"missing local_override bool for {key}"
);
}
assert!(
s["structure_first"].get("local_override").is_none(),
"structure_first must not carry local_override"
);
}
}