use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
use crate::config::{ApprovalPolicy, Config, ConfigBuilder, ConfigProfile, ToolOverrideProfile};
use crate::tools::SandboxPolicy;
fn default_schema_version() -> u32 {
1
}
pub fn expand_env_vars(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(start) = rest.find("${") {
out.push_str(&rest[..start]);
let after = &rest[start + 2..];
match after.find('}') {
Some(end) => {
let var_name = &after[..end];
match std::env::var(var_name) {
Ok(v) => out.push_str(&v),
Err(_) => out.push_str(&rest[start..start + 2 + end + 1]),
}
rest = &after[end + 1..];
}
None => {
out.push_str(&rest[start..]);
rest = "";
break;
}
}
}
out.push_str(rest);
out
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct HarnessConfig {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub extends: Option<String>,
#[serde(default)]
pub core: CoreSection,
#[serde(default)]
pub capabilities: BTreeMap<String, CapabilityConfig>,
#[serde(default)]
pub experimental: serde_json::Map<String, serde_json::Value>,
}
impl Default for HarnessConfig {
fn default() -> Self {
HarnessConfig {
schema_version: default_schema_version(),
extends: None,
core: CoreSection::default(),
capabilities: BTreeMap::new(),
experimental: serde_json::Map::new(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreSection {
pub model: Option<String>,
pub base_url: Option<String>,
pub api_key_env: Option<String>,
pub api_key_cmd: Option<String>,
pub effort: Option<String>,
pub temperature: Option<f32>,
pub max_tokens: Option<u32>,
pub max_iterations: Option<usize>,
pub max_total_output_tokens: Option<u64>,
pub max_tool_output_bytes: Option<usize>,
pub parallel_tool_calls: Option<bool>,
pub shell_env_snapshot: Option<bool>,
pub system_prompt: Option<String>,
pub append_system_prompt: Option<String>,
pub project_context: Option<bool>,
pub env_context: Option<bool>,
pub context_injections: Option<bool>,
pub nested_instructions: Option<bool>,
pub instruction_imports: Option<bool>,
pub project_root_markers: Option<Vec<String>>,
pub hot_reload: Option<bool>,
pub project_doc_max_bytes: Option<usize>,
pub additional_dirs: Option<Vec<String>>,
pub extra_headers: Option<HashMap<String, String>>,
pub extra_body: Option<serde_json::Map<String, serde_json::Value>>,
pub doom_loop_threshold: Option<u32>,
#[serde(default)]
pub model_switch: CoreModelSwitchConfig,
#[serde(default)]
pub retry: CoreRetryConfig,
#[serde(default)]
pub tools: CoreToolsConfig,
#[serde(default)]
pub skills: CoreSkillsConfig,
#[serde(default)]
pub prompts: BTreeMap<String, String>,
#[serde(default)]
pub compaction: CoreCompactionConfig,
#[serde(default)]
pub session: CoreSessionConfig,
#[serde(default)]
pub steering: CoreSteeringConfig,
#[serde(default)]
pub output: CoreOutputConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreModelSwitchConfig {
pub allow_switch: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreRetryConfig {
pub enabled: Option<bool>,
pub max_retries: Option<u32>,
pub base_delay_ms: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreToolsConfig {
pub enabled: Option<Vec<String>>,
pub schema_tier: Option<String>,
#[serde(default)]
pub read_file: ReadFileToolConfig,
#[serde(default)]
pub edit_file: EditFileToolConfig,
#[serde(default)]
pub bash: BashToolConfig,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct ReadFileToolConfig {
pub multimodal: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct EditFileToolConfig {
pub require_read_before_edit: Option<bool>,
pub notebook_aware: Option<bool>,
pub schema_tier: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct BashToolConfig {
pub enabled: Option<bool>,
pub description: Option<String>,
pub schema_tier: Option<String>,
pub timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreSkillsConfig {
pub enabled: Option<bool>,
pub dirs: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreCompactionConfig {
pub enabled: Option<bool>,
pub after_messages: Option<usize>,
pub reserve_tokens: Option<usize>,
pub keep_recent_tokens: Option<usize>,
pub summarize: Option<bool>,
pub focus_instructions: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreSessionConfig {
pub dir: Option<String>,
pub name: Option<String>,
pub persist: Option<bool>,
pub retention_days: Option<u32>,
pub export_format: Option<String>,
pub auto_title: Option<bool>,
pub git_metadata: Option<bool>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreSteeringConfig {
pub steering_mode: Option<String>,
pub follow_up_mode: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CoreOutputConfig {
pub format: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize, Serialize)]
pub struct CapabilityConfig {
pub enabled: Option<bool>,
#[serde(flatten)]
pub settings: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug)]
pub enum HarnessConfigError {
Toml(toml::de::Error),
Json(serde_json::Error),
UnsupportedSchemaVersion(u32),
}
impl std::fmt::Display for HarnessConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HarnessConfigError::Toml(e) => write!(f, "{e}"),
HarnessConfigError::Json(e) => write!(f, "{e}"),
HarnessConfigError::UnsupportedSchemaVersion(v) => write!(
f,
"unsupported schema_version {v}; this build only understands schema_version = 1"
),
}
}
}
impl std::error::Error for HarnessConfigError {}
impl HarnessConfig {
pub fn from_toml_str(s: &str) -> Result<Self, HarnessConfigError> {
let hc: HarnessConfig = toml::from_str(s).map_err(HarnessConfigError::Toml)?;
hc.check_schema_version()?;
Ok(hc)
}
pub fn from_json_str(s: &str) -> Result<Self, HarnessConfigError> {
let hc: HarnessConfig = serde_json::from_str(s).map_err(HarnessConfigError::Json)?;
hc.check_schema_version()?;
Ok(hc)
}
fn check_schema_version(&self) -> Result<(), HarnessConfigError> {
if self.schema_version != 1 {
return Err(HarnessConfigError::UnsupportedSchemaVersion(
self.schema_version,
));
}
Ok(())
}
pub fn to_config_profile(&self) -> ConfigProfile {
let c = &self.core;
let mut tool_overrides = HashMap::new();
if c.tools.bash.enabled.is_some()
|| c.tools.bash.description.is_some()
|| c.tools.bash.schema_tier.is_some()
|| c.tools.bash.timeout_secs.is_some()
{
tool_overrides.insert(
"bash".to_string(),
ToolOverrideProfile {
enabled: c.tools.bash.enabled,
description: c.tools.bash.description.clone(),
schema_tier: c.tools.bash.schema_tier.clone(),
timeout_secs: c.tools.bash.timeout_secs,
},
);
}
if c.tools.edit_file.schema_tier.is_some() {
tool_overrides.insert(
"edit_file".to_string(),
ToolOverrideProfile {
enabled: None,
description: None,
schema_tier: c.tools.edit_file.schema_tier.clone(),
timeout_secs: None,
},
);
}
ConfigProfile {
model: c.model.clone(),
base_url: c.base_url.as_deref().map(expand_env_vars),
api_key_env: c.api_key_env.clone(),
api_key_cmd: c.api_key_cmd.clone(),
system_prompt: c.system_prompt.as_deref().map(expand_env_vars),
append_system_prompt: c.append_system_prompt.as_deref().map(expand_env_vars),
temperature: c.temperature,
max_tokens: c.max_tokens,
effort: c.effort.clone(),
sandbox: None,
approval: None,
project_context: c.project_context,
max_iterations: c.max_iterations,
additional_dirs: c
.additional_dirs
.as_ref()
.map(|dirs| dirs.iter().map(|d| expand_env_vars(d)).collect()),
compact_after_messages: c.compaction.after_messages,
cache_plan: None,
tool_advertising: None,
tool_advertising_core: None,
schema_tier: c.tools.schema_tier.clone(),
auto_approved_tools: None,
tool_deny_patterns: None,
tool_allow_patterns: None,
extra_headers: c.extra_headers.as_ref().map(|headers| {
headers
.iter()
.map(|(k, v)| (k.clone(), expand_env_vars(v)))
.collect()
}),
extra_body: c.extra_body.as_ref().map(|body| {
body.iter()
.map(|(k, v)| {
let v = match v {
serde_json::Value::String(s) => {
serde_json::Value::String(expand_env_vars(s))
}
other => other.clone(),
};
(k.clone(), v)
})
.collect()
}),
max_tool_output_bytes: c.max_tool_output_bytes,
max_total_output_tokens: c.max_total_output_tokens,
prompts: if c.prompts.is_empty() {
None
} else {
Some(
c.prompts
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
},
tool_overrides: if tool_overrides.is_empty() {
None
} else {
Some(tool_overrides)
},
env_context: c.env_context,
project_root_markers: c.project_root_markers.clone(),
project_doc_max_bytes: c.project_doc_max_bytes,
instruction_imports: c.instruction_imports,
retry_enabled: c.retry.enabled,
retry_max_retries: c.retry.max_retries,
retry_base_delay_ms: c.retry.base_delay_ms,
compaction_reserve_tokens: c.compaction.reserve_tokens.map(|n| n as u64),
compaction_keep_recent_tokens: c.compaction.keep_recent_tokens.map(|n| n as u64),
compaction_focus_instructions: c.compaction.focus_instructions.clone(),
auto_title: c.session.auto_title,
steering_mode: c.steering.steering_mode.clone(),
follow_up_mode: c.steering.follow_up_mode.clone(),
read_file_multimodal: c.tools.read_file.multimodal,
edit_file_require_read_before_edit: c.tools.edit_file.require_read_before_edit,
edit_file_notebook_aware: c.tools.edit_file.notebook_aware,
shell_env_snapshot: c.shell_env_snapshot,
doom_loop_threshold: c.doom_loop_threshold,
nested_instructions: c.nested_instructions,
model_switch_allow_switch: c.model_switch.allow_switch,
context_injections: c.context_injections,
compaction_enabled: c.compaction.enabled,
parallel_tool_calls: c.parallel_tool_calls,
session_git_metadata: c.session.git_metadata,
session_dir: c.session.dir.clone(),
session_persist: c.session.persist,
session_name: c.session.name.clone(),
session_retention_days: c.session.retention_days,
session_export_format: c.session.export_format.clone(),
}
}
pub fn resolve_core(&self) -> Config {
ConfigBuilder::default()
.apply_profile(&self.to_config_profile())
.build()
}
pub fn overlay(&self, over: &HarnessConfig) -> HarnessConfig {
HarnessConfig {
schema_version: over.schema_version,
extends: over.extends.clone().or_else(|| self.extends.clone()),
core: merge_core(&self.core, &over.core),
capabilities: merge_capabilities(&self.capabilities, &over.capabilities),
experimental: {
let mut e = self.experimental.clone();
merge_json_object(&mut e, &over.experimental);
e
},
}
}
}
macro_rules! merge_opt {
($base:expr, $over:expr, $field:ident) => {
$over.$field.clone().or_else(|| $base.$field.clone())
};
}
fn merge_core(base: &CoreSection, over: &CoreSection) -> CoreSection {
CoreSection {
model: merge_opt!(base, over, model),
base_url: merge_opt!(base, over, base_url),
api_key_env: merge_opt!(base, over, api_key_env),
api_key_cmd: merge_opt!(base, over, api_key_cmd),
effort: merge_opt!(base, over, effort),
temperature: merge_opt!(base, over, temperature),
max_tokens: merge_opt!(base, over, max_tokens),
max_iterations: merge_opt!(base, over, max_iterations),
max_total_output_tokens: merge_opt!(base, over, max_total_output_tokens),
max_tool_output_bytes: merge_opt!(base, over, max_tool_output_bytes),
parallel_tool_calls: merge_opt!(base, over, parallel_tool_calls),
shell_env_snapshot: merge_opt!(base, over, shell_env_snapshot),
system_prompt: merge_opt!(base, over, system_prompt),
append_system_prompt: merge_opt!(base, over, append_system_prompt),
project_context: merge_opt!(base, over, project_context),
env_context: merge_opt!(base, over, env_context),
context_injections: merge_opt!(base, over, context_injections),
nested_instructions: merge_opt!(base, over, nested_instructions),
instruction_imports: merge_opt!(base, over, instruction_imports),
project_root_markers: merge_opt!(base, over, project_root_markers),
hot_reload: merge_opt!(base, over, hot_reload),
project_doc_max_bytes: merge_opt!(base, over, project_doc_max_bytes),
doom_loop_threshold: merge_opt!(base, over, doom_loop_threshold),
additional_dirs: merge_opt!(base, over, additional_dirs),
extra_headers: match (&base.extra_headers, &over.extra_headers) {
(Some(b), Some(o)) => {
let mut m = b.clone();
m.extend(o.clone());
Some(m)
}
(None, Some(o)) => Some(o.clone()),
(b, None) => b.clone(),
},
extra_body: match (&base.extra_body, &over.extra_body) {
(Some(b), Some(o)) => {
let mut m = b.clone();
for (k, v) in o {
m.insert(k.clone(), v.clone());
}
Some(m)
}
(None, Some(o)) => Some(o.clone()),
(b, None) => b.clone(),
},
model_switch: CoreModelSwitchConfig {
allow_switch: merge_opt!(base.model_switch, over.model_switch, allow_switch),
},
retry: CoreRetryConfig {
enabled: merge_opt!(base.retry, over.retry, enabled),
max_retries: merge_opt!(base.retry, over.retry, max_retries),
base_delay_ms: merge_opt!(base.retry, over.retry, base_delay_ms),
},
tools: CoreToolsConfig {
enabled: merge_opt!(base.tools, over.tools, enabled),
schema_tier: merge_opt!(base.tools, over.tools, schema_tier),
read_file: ReadFileToolConfig {
multimodal: merge_opt!(base.tools.read_file, over.tools.read_file, multimodal),
},
edit_file: EditFileToolConfig {
require_read_before_edit: merge_opt!(
base.tools.edit_file,
over.tools.edit_file,
require_read_before_edit
),
notebook_aware: merge_opt!(
base.tools.edit_file,
over.tools.edit_file,
notebook_aware
),
schema_tier: merge_opt!(base.tools.edit_file, over.tools.edit_file, schema_tier),
},
bash: BashToolConfig {
enabled: merge_opt!(base.tools.bash, over.tools.bash, enabled),
description: merge_opt!(base.tools.bash, over.tools.bash, description),
schema_tier: merge_opt!(base.tools.bash, over.tools.bash, schema_tier),
timeout_secs: merge_opt!(base.tools.bash, over.tools.bash, timeout_secs),
},
},
skills: CoreSkillsConfig {
enabled: merge_opt!(base.skills, over.skills, enabled),
dirs: merge_opt!(base.skills, over.skills, dirs),
},
prompts: {
let mut p = base.prompts.clone();
for (k, v) in &over.prompts {
p.insert(k.clone(), v.clone());
}
p
},
compaction: CoreCompactionConfig {
enabled: merge_opt!(base.compaction, over.compaction, enabled),
after_messages: merge_opt!(base.compaction, over.compaction, after_messages),
reserve_tokens: merge_opt!(base.compaction, over.compaction, reserve_tokens),
keep_recent_tokens: merge_opt!(base.compaction, over.compaction, keep_recent_tokens),
summarize: merge_opt!(base.compaction, over.compaction, summarize),
focus_instructions: merge_opt!(base.compaction, over.compaction, focus_instructions),
},
session: CoreSessionConfig {
dir: merge_opt!(base.session, over.session, dir),
name: merge_opt!(base.session, over.session, name),
persist: merge_opt!(base.session, over.session, persist),
retention_days: merge_opt!(base.session, over.session, retention_days),
export_format: merge_opt!(base.session, over.session, export_format),
auto_title: merge_opt!(base.session, over.session, auto_title),
git_metadata: merge_opt!(base.session, over.session, git_metadata),
},
steering: CoreSteeringConfig {
steering_mode: merge_opt!(base.steering, over.steering, steering_mode),
follow_up_mode: merge_opt!(base.steering, over.steering, follow_up_mode),
},
output: CoreOutputConfig {
format: merge_opt!(base.output, over.output, format),
},
}
}
fn merge_json_object(
base: &mut serde_json::Map<String, serde_json::Value>,
over: &serde_json::Map<String, serde_json::Value>,
) {
for (k, v) in over {
match (base.get_mut(k), v) {
(Some(serde_json::Value::Object(b)), serde_json::Value::Object(o)) => {
merge_json_object(b, o);
}
_ => {
base.insert(k.clone(), v.clone());
}
}
}
}
fn merge_capabilities(
base: &BTreeMap<String, CapabilityConfig>,
over: &BTreeMap<String, CapabilityConfig>,
) -> BTreeMap<String, CapabilityConfig> {
let mut out = base.clone();
for (name, ov) in over {
match out.get_mut(name) {
Some(existing) => {
existing.enabled = ov.enabled.or(existing.enabled);
merge_json_object(&mut existing.settings, &ov.settings);
}
None => {
out.insert(name.clone(), ov.clone());
}
}
}
out
}
pub fn merge_reduction_capability(
trusted: Option<&CapabilityConfig>,
project: Option<&CapabilityConfig>,
) -> Option<CapabilityConfig> {
fn narrowing_bool(trusted: Option<bool>, project: Option<bool>) -> Option<bool> {
match (trusted, project) {
(Some(false), _) | (_, Some(false)) => Some(false),
(_, Some(true)) => Some(true),
(Some(true), None) => Some(true),
(None, None) => None,
}
}
const BOOLEAN_GATES: &[&str] = &[
"stale_reads",
"diff_reads",
"duplicates",
"tool_input_elision",
"supersede",
"normalize_output",
"image_redaction",
"span_summaries",
"handoff",
];
match (trusted, project) {
(None, None) => None,
(Some(t), None) => Some(t.clone()),
(None, Some(p)) => Some(p.clone()),
(Some(t), Some(p)) => {
let mut merged = t.clone();
merged.enabled = narrowing_bool(t.enabled, p.enabled);
merge_json_object(&mut merged.settings, &p.settings);
for key in BOOLEAN_GATES {
let trusted_value = t.settings.get(*key).and_then(|v| v.as_bool());
let project_value = p.settings.get(*key).and_then(|v| v.as_bool());
if let Some(value) = narrowing_bool(trusted_value, project_value) {
merged
.settings
.insert((*key).to_string(), serde_json::Value::Bool(value));
}
}
Some(merged)
}
}
}
fn nested_str_array(
settings: &serde_json::Map<String, serde_json::Value>,
path: &[&str],
) -> Vec<String> {
let Some((last, dirs)) = path.split_last() else {
return Vec::new();
};
let mut cur = settings;
for seg in dirs {
match cur.get(*seg).and_then(|v| v.as_object()) {
Some(m) => cur = m,
None => return Vec::new(),
}
}
cur.get(*last)
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
fn set_nested_str_array(
settings: &mut serde_json::Map<String, serde_json::Value>,
path: &[&str],
value: Vec<String>,
) {
let Some((last, dirs)) = path.split_last() else {
return;
};
let mut cur = settings;
for seg in dirs {
let entry = cur
.entry((*seg).to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if !entry.is_object() {
*entry = serde_json::Value::Object(serde_json::Map::new());
}
cur = entry.as_object_mut().expect("just ensured object above");
}
cur.insert(
(*last).to_string(),
serde_json::Value::Array(value.into_iter().map(serde_json::Value::String).collect()),
);
}
fn union_nested_str_array(
trusted: &serde_json::Map<String, serde_json::Value>,
project: &serde_json::Map<String, serde_json::Value>,
merged: &mut serde_json::Map<String, serde_json::Value>,
path: &[&str],
) {
let trusted_vals = nested_str_array(trusted, path);
let project_vals = nested_str_array(project, path);
if trusted_vals.is_empty() && project_vals.is_empty() {
return;
}
let mut union = trusted_vals;
for v in project_vals {
if !union.contains(&v) {
union.push(v);
}
}
set_nested_str_array(merged, path, union);
}
pub fn merge_permissions_capability(
trusted: Option<&CapabilityConfig>,
project: Option<&CapabilityConfig>,
) -> Option<CapabilityConfig> {
match (trusted, project) {
(None, None) => None,
(Some(t), None) => Some(t.clone()),
(None, Some(p)) => Some(p.clone()),
(Some(t), Some(p)) => {
let mut merged = t.clone();
merged.enabled = p.enabled.or(t.enabled);
merge_json_object(&mut merged.settings, &p.settings);
match merge_sandbox_value(t.settings.get("sandbox"), p.settings.get("sandbox")) {
Some(v) => {
merged.settings.insert("sandbox".to_string(), v);
}
None => {
merged.settings.remove("sandbox");
}
}
union_nested_str_array(
&t.settings,
&p.settings,
&mut merged.settings,
&["rules", "deny"],
);
union_nested_str_array(
&t.settings,
&p.settings,
&mut merged.settings,
&["rules", "ask"],
);
union_nested_str_array(
&t.settings,
&p.settings,
&mut merged.settings,
&["protected_paths", "paths"],
);
Some(merged)
}
}
}
fn merge_sandbox_value(
base: Option<&serde_json::Value>,
project: Option<&serde_json::Value>,
) -> Option<serde_json::Value> {
fn to_table(v: &serde_json::Value) -> serde_json::Map<String, serde_json::Value> {
match v {
serde_json::Value::String(s) => {
let mut m = serde_json::Map::new();
m.insert("tier".to_string(), serde_json::Value::String(s.clone()));
m
}
serde_json::Value::Object(o) => o.clone(),
_ => serde_json::Map::new(),
}
}
match (base, project) {
(None, None) => None,
(Some(b), None) => Some(b.clone()),
(None, Some(p)) => Some(p.clone()),
(Some(b), Some(p)) => {
let mut merged = to_table(b);
let proj_table = to_table(p);
merge_json_object(&mut merged, &proj_table);
Some(serde_json::Value::Object(merged))
}
}
}
fn parse_sandbox_str(s: &str) -> Option<SandboxPolicy> {
match s.replace('_', "-").to_ascii_lowercase().as_str() {
"read-only" | "readonly" => Some(SandboxPolicy::ReadOnly),
"workspace-write" | "workspace" => Some(SandboxPolicy::WorkspaceWrite),
"danger-full-access" | "full" => Some(SandboxPolicy::DangerFullAccess),
_ => None,
}
}
fn parse_approval_str(s: &str) -> Option<ApprovalPolicy> {
match s.replace('_', "-").to_ascii_lowercase().as_str() {
"never" => Some(ApprovalPolicy::Never),
"on-request" | "onrequest" => Some(ApprovalPolicy::OnRequest),
"untrusted" => Some(ApprovalPolicy::Untrusted),
"model-requested" | "modelrequested" => Some(ApprovalPolicy::ModelRequested),
_ => None,
}
}
fn is_loosening_sandbox_str(s: &str) -> bool {
parse_sandbox_str(s) == Some(SandboxPolicy::DangerFullAccess)
}
fn is_loosening_approval_str(s: &str) -> bool {
parse_approval_str(s) == Some(ApprovalPolicy::Never)
}
fn sandbox_rank(p: SandboxPolicy) -> u8 {
match p {
SandboxPolicy::ReadOnly => 0,
SandboxPolicy::WorkspaceWrite => 1,
SandboxPolicy::DangerFullAccess => 2,
}
}
fn approval_rank(p: ApprovalPolicy) -> u8 {
match p {
ApprovalPolicy::Untrusted => 0,
ApprovalPolicy::OnRequest => 1,
ApprovalPolicy::ModelRequested => 2,
ApprovalPolicy::Never => 3,
}
}
fn permissions_sandbox_raw(hc: &HarnessConfig) -> Option<String> {
let cap = hc.capabilities.get("permissions")?;
match cap.settings.get("sandbox")? {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Object(o) => o.get("tier").and_then(|v| v.as_str()).map(String::from),
_ => None,
}
}
fn permissions_approval_raw(hc: &HarnessConfig) -> Option<String> {
hc.capabilities
.get("permissions")
.and_then(|cap| cap.settings.get("approval"))
.and_then(|v| v.as_str())
.map(String::from)
}
fn effective_sandbox(hc: &HarnessConfig) -> SandboxPolicy {
permissions_sandbox_raw(hc)
.as_deref()
.and_then(parse_sandbox_str)
.unwrap_or(SandboxPolicy::DangerFullAccess)
}
fn effective_approval(hc: &HarnessConfig) -> ApprovalPolicy {
match permissions_approval_raw(hc) {
None => ApprovalPolicy::Never,
Some(raw) => parse_approval_str(&raw).unwrap_or(ApprovalPolicy::Untrusted),
}
}
const PROJECT_FORBIDDEN_CAPABILITY_TABLES: &[&str] =
&["hooks", "plugins", "server", "integrations", "trust"];
const PROJECT_ALLOWED_CAPABILITY_ENABLE: &[&str] = &["reduction"];
fn is_safe_project_dir(d: &str) -> bool {
if d.contains("${") {
return false;
}
if d.starts_with('~') {
return false;
}
let path = std::path::Path::new(d);
if path.is_absolute() {
return false;
}
!path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
}
pub fn sanitize_for_project(hc: &HarnessConfig) -> (HarnessConfig, Vec<String>) {
let mut dropped = Vec::new();
let mut out = hc.clone();
if out.core.base_url.take().is_some() {
dropped.push("core.base_url".to_string());
}
if out.core.api_key_env.take().is_some() {
dropped.push("core.api_key_env".to_string());
}
if out.core.api_key_cmd.take().is_some() {
dropped.push("core.api_key_cmd".to_string());
}
if out.core.extra_headers.take().is_some() {
dropped.push("core.extra_headers".to_string());
}
if out.core.extra_body.take().is_some() {
dropped.push("core.extra_body".to_string());
}
if out.core.system_prompt.take().is_some() {
dropped.push("core.system_prompt".to_string());
}
if out.core.append_system_prompt.take().is_some() {
dropped.push("core.append_system_prompt".to_string());
}
if out.core.compaction.focus_instructions.take().is_some() {
dropped.push("core.compaction.focus_instructions".to_string());
}
if !out.core.prompts.is_empty() {
out.core.prompts.clear();
dropped.push("core.prompts".to_string());
}
if out.core.session.dir.take().is_some() {
dropped.push("core.session.dir".to_string());
}
if out.core.session.name.take().is_some() {
dropped.push("core.session.name".to_string());
}
if out.core.session.persist.take().is_some() {
dropped.push("core.session.persist".to_string());
}
if out.core.session.retention_days.take().is_some() {
dropped.push("core.session.retention_days".to_string());
}
if out.core.session.export_format.take().is_some() {
dropped.push("core.session.export_format".to_string());
}
if out.core.session.git_metadata.take().is_some() {
dropped.push("core.session.git_metadata".to_string());
}
if let Some(dirs) = &out.core.additional_dirs {
let (kept, rejected): (Vec<String>, Vec<String>) =
dirs.iter().cloned().partition(|d| is_safe_project_dir(d));
if !rejected.is_empty() {
dropped.push(format!("core.additional_dirs ({})", rejected.join(", ")));
out.core.additional_dirs = if kept.is_empty() { None } else { Some(kept) };
}
}
if let Some(e) = &out.extends {
if crate::presets::lookup(e).is_none() {
dropped.push("extends (path)".to_string());
out.extends = None;
}
}
if !out.experimental.is_empty() {
out.experimental.clear();
dropped.push("experimental".to_string());
}
for name in PROJECT_FORBIDDEN_CAPABILITY_TABLES {
if out.capabilities.remove(*name).is_some() {
dropped.push(format!("capabilities.{name}"));
}
}
if let Some(cap) = out.capabilities.get_mut("mcp") {
if cap.settings.remove("servers").is_some() {
dropped.push("capabilities.mcp.servers".to_string());
}
if matches!(
cap.settings.get("serve"),
Some(serde_json::Value::Bool(true))
) {
cap.settings.remove("serve");
dropped.push("capabilities.mcp.serve".to_string());
}
}
if let Some(cap) = out.capabilities.get_mut("notify") {
if cap.settings.remove("email").is_some() {
dropped.push("capabilities.notify.email".to_string());
}
}
if let Some(cap) = out.capabilities.get_mut("lsp") {
if cap.settings.remove("servers").is_some() {
dropped.push("capabilities.lsp.servers".to_string());
}
}
if let Some(cap) = out.capabilities.get_mut("formatters") {
let formatter_keys: Vec<String> = cap
.settings
.keys()
.filter(|k| !matches!(k.as_str(), "diff_back" | "timeout_secs"))
.cloned()
.collect();
for key in formatter_keys {
cap.settings.remove(&key);
dropped.push(format!("capabilities.formatters.{key}"));
}
if matches!(
cap.settings.get("diff_back"),
Some(serde_json::Value::Bool(false))
) {
cap.settings.remove("diff_back");
dropped.push("capabilities.formatters.diff_back".to_string());
}
}
if let Some(cap) = out.capabilities.get_mut("permissions") {
match cap.settings.get("sandbox").cloned() {
Some(serde_json::Value::String(sb)) if is_loosening_sandbox_str(&sb) => {
cap.settings.remove("sandbox");
dropped.push("capabilities.permissions.sandbox".to_string());
}
Some(serde_json::Value::Object(_)) => {
if let Some(tbl) = cap
.settings
.get_mut("sandbox")
.and_then(|v| v.as_object_mut())
{
if let Some(tier) = tbl.get("tier").and_then(|v| v.as_str()).map(String::from) {
if is_loosening_sandbox_str(&tier) {
tbl.remove("tier");
dropped.push("capabilities.permissions.sandbox.tier".to_string());
}
}
if matches!(tbl.get("enabled"), Some(serde_json::Value::Bool(false))) {
tbl.remove("enabled");
dropped.push("capabilities.permissions.sandbox.enabled".to_string());
}
if let Some(esc) = tbl.get("escalation").and_then(|v| v.as_str()) {
if crate::sandbox::SandboxEscalation::parse(esc)
== Some(crate::sandbox::SandboxEscalation::Allow)
{
tbl.remove("escalation");
dropped.push("capabilities.permissions.sandbox.escalation".to_string());
}
}
if let Some(ep) = tbl.get("env_policy").and_then(|v| v.as_str()) {
if crate::sandbox::SandboxEnvPolicy::parse(ep)
== Some(crate::sandbox::SandboxEnvPolicy::Inherit)
{
tbl.remove("env_policy");
dropped.push("capabilities.permissions.sandbox.env_policy".to_string());
}
}
if let Some(net) = tbl.get_mut("network").and_then(|v| v.as_object_mut()) {
for k in ["allow_domains", "deny_domains"] {
if net.remove(k).is_some() {
dropped
.push(format!("capabilities.permissions.sandbox.network.{k}"));
}
}
if matches!(net.get("enabled"), Some(serde_json::Value::Bool(false))) {
net.remove("enabled");
dropped.push(
"capabilities.permissions.sandbox.network.enabled".to_string(),
);
}
}
}
}
_ => {}
}
if let Some(ap) = cap.settings.get("approval").and_then(|v| v.as_str()) {
if is_loosening_approval_str(ap) {
cap.settings.remove("approval");
dropped.push("capabilities.permissions.approval".to_string());
}
}
if cap.settings.remove("auto_approved_tools").is_some() {
dropped.push("capabilities.permissions.auto_approved_tools".to_string());
}
if let Some(rules) = cap
.settings
.get_mut("rules")
.and_then(|v| v.as_object_mut())
{
if rules.remove("allow").is_some() {
dropped.push("capabilities.permissions.rules.allow".to_string());
}
}
}
for (name, cap) in out.capabilities.iter_mut() {
if cap.enabled == Some(true) && !PROJECT_ALLOWED_CAPABILITY_ENABLE.contains(&name.as_str())
{
cap.enabled = None;
dropped.push(format!("capabilities.{name}.enabled"));
}
}
(out, dropped)
}
pub fn clamp_project_permissions(
base: &HarnessConfig,
sanitized_project: &HarnessConfig,
merged: &mut HarnessConfig,
) -> Vec<String> {
let mut clamped = Vec::new();
let base_sandbox = effective_sandbox(base);
let base_approval = effective_approval(base);
if let Some(raw) = permissions_sandbox_raw(sanitized_project) {
if let Some(parsed) = parse_sandbox_str(&raw) {
if sandbox_rank(parsed) > sandbox_rank(base_sandbox) {
clamped.push("capabilities.permissions.sandbox".to_string());
set_sandbox_tier(merged, permissions_sandbox_raw(base));
}
}
}
if let Some(raw) = permissions_approval_raw(sanitized_project) {
if let Some(parsed) = parse_approval_str(&raw) {
if approval_rank(parsed) > approval_rank(base_approval) {
clamped.push("capabilities.permissions.approval".to_string());
set_permissions_approval_raw(merged, permissions_approval_raw(base));
}
}
}
let base_escalation = permissions_sandbox_escalation_raw(base)
.as_deref()
.and_then(crate::sandbox::SandboxEscalation::parse)
.unwrap_or_default();
if let Some(raw) = permissions_sandbox_escalation_raw(sanitized_project) {
if let Some(parsed) = crate::sandbox::SandboxEscalation::parse(&raw) {
if parsed.rank() > base_escalation.rank() {
clamped.push("capabilities.permissions.sandbox.escalation".to_string());
set_permissions_sandbox_escalation_raw(
merged,
permissions_sandbox_escalation_raw(base),
);
}
}
}
let base_env_policy = permissions_sandbox_env_policy_raw(base)
.as_deref()
.and_then(crate::sandbox::SandboxEnvPolicy::parse)
.unwrap_or_default();
if let Some(raw) = permissions_sandbox_env_policy_raw(sanitized_project) {
if let Some(parsed) = crate::sandbox::SandboxEnvPolicy::parse(&raw) {
if parsed.rank() > base_env_policy.rank() {
clamped.push("capabilities.permissions.sandbox.env_policy".to_string());
set_permissions_sandbox_env_policy_raw(
merged,
permissions_sandbox_env_policy_raw(base),
);
}
}
}
let merged_sandbox = effective_sandbox(merged);
if sandbox_rank(merged_sandbox) > sandbox_rank(base_sandbox)
&& !clamped
.iter()
.any(|c| c == "capabilities.permissions.sandbox")
{
clamped.push("capabilities.permissions.sandbox".to_string());
set_sandbox_tier(merged, permissions_sandbox_raw(base));
}
clamped
}
fn permissions_sandbox_escalation_raw(hc: &HarnessConfig) -> Option<String> {
hc.capabilities
.get("permissions")?
.settings
.get("sandbox")?
.as_object()?
.get("escalation")?
.as_str()
.map(String::from)
}
fn permissions_sandbox_env_policy_raw(hc: &HarnessConfig) -> Option<String> {
hc.capabilities
.get("permissions")?
.settings
.get("sandbox")?
.as_object()?
.get("env_policy")?
.as_str()
.map(String::from)
}
fn set_permissions_sandbox_escalation_raw(hc: &mut HarnessConfig, value: Option<String>) {
set_permissions_sandbox_subkey_raw(hc, "escalation", value);
}
fn set_permissions_sandbox_env_policy_raw(hc: &mut HarnessConfig, value: Option<String>) {
set_permissions_sandbox_subkey_raw(hc, "env_policy", value);
}
fn set_permissions_sandbox_subkey_raw(hc: &mut HarnessConfig, key: &str, value: Option<String>) {
let cap = hc
.capabilities
.entry("permissions".to_string())
.or_default();
let entry = cap
.settings
.entry("sandbox".to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if !entry.is_object() {
return;
}
let obj = entry.as_object_mut().expect("just checked is_object");
match value {
Some(v) => {
obj.insert(key.to_string(), serde_json::Value::String(v));
}
None => {
obj.remove(key);
}
}
}
fn set_sandbox_tier(hc: &mut HarnessConfig, value: Option<String>) {
let cap = hc
.capabilities
.entry("permissions".to_string())
.or_default();
if let Some(serde_json::Value::Object(obj)) = cap.settings.get_mut("sandbox") {
match value {
Some(v) => {
obj.insert("tier".to_string(), serde_json::Value::String(v));
}
None => {
obj.remove("tier");
}
}
return;
}
match value {
Some(v) => {
cap.settings
.insert("sandbox".to_string(), serde_json::Value::String(v));
}
None => {
cap.settings.remove("sandbox");
}
}
}
fn set_permissions_approval_raw(hc: &mut HarnessConfig, value: Option<String>) {
let cap = hc
.capabilities
.entry("permissions".to_string())
.or_default();
match value {
Some(v) => {
cap.settings
.insert("approval".to_string(), serde_json::Value::String(v));
}
None => {
cap.settings.remove("approval");
}
}
}
pub const MODULE_NAMES: &[&str] = &[
"tools_search",
"tools_apply_patch",
"tools_persistent_shell",
"tools_background",
"tools_web",
"tools_question",
"todos",
"plan_mode",
"subagents",
"permissions",
"trust",
"mcp",
"hooks",
"plugins",
"memory",
"checkpoint",
"session_tree",
"session_share",
"reduction",
"deferred_tools",
"cache",
"model_catalog",
"model_oauth",
"lsp",
"formatters",
"tui",
"server",
"notify",
"structured_output",
"telemetry",
"integrations",
];
pub const NESTED_MODULE_NAMES: &[&str] = &[
"permissions.rules",
"permissions.sandbox",
"permissions.protected_paths",
];
pub fn module_enabled(hc: &HarnessConfig, module: &str) -> bool {
let mut parts = module.splitn(2, '.');
let top = parts.next().unwrap_or("");
let Some(cap) = hc.capabilities.get(top) else {
return false;
};
match parts.next() {
None => cap.enabled.unwrap_or(false),
Some(sub) => cap
.settings
.get(sub)
.and_then(|v| v.as_object())
.and_then(|o| o.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
}
}
pub(crate) fn module_setting_bool(hc: &HarnessConfig, top: &str, key: &str) -> bool {
hc.capabilities
.get(top)
.and_then(|c| c.settings.get(key))
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
fn module_setting_str<'a>(hc: &'a HarnessConfig, top: &str, key: &str) -> Option<&'a str> {
hc.capabilities
.get(top)
.and_then(|c| c.settings.get(key))
.and_then(|v| v.as_str())
}
fn effective_tools_enabled(hc: &HarnessConfig) -> Vec<String> {
hc.core.tools.enabled.clone().unwrap_or_else(|| {
["read_file", "bash", "edit_file", "write_file"]
.iter()
.map(|s| s.to_string())
.collect()
})
}
fn activation_set(hc: &HarnessConfig) -> BTreeMap<String, bool> {
let mut set = BTreeMap::new();
for name in MODULE_NAMES {
set.insert((*name).to_string(), module_enabled(hc, name));
}
for name in NESTED_MODULE_NAMES {
set.insert((*name).to_string(), module_enabled(hc, name));
}
set
}
fn validate_modules(
hc: &HarnessConfig,
preset_baseline: Option<&HarnessConfig>,
) -> Result<Vec<String>, ResolveError> {
let mut warnings = Vec::new();
let tools_enabled = effective_tools_enabled(hc);
let has = |name: &str| tools_enabled.iter().any(|t| t == name);
if module_enabled(hc, "subagents") && module_setting_bool(hc, "subagents", "background") {
require(
module_enabled(hc, "permissions"),
"subagents (background)",
"permissions",
)?;
}
if module_enabled(hc, "permissions.rules") {
require(
module_enabled(hc, "permissions"),
"permissions.rules",
"permissions",
)?;
}
if module_enabled(hc, "lsp") {
require(
has("edit_file") && has("write_file"),
"lsp",
"core.tools.enabled (edit_file, write_file)",
)?;
}
if module_enabled(hc, "hooks") {
require(module_enabled(hc, "trust"), "hooks", "trust")?;
}
if module_enabled(hc, "plugins") {
require(module_enabled(hc, "trust"), "plugins", "trust")?;
}
if module_enabled(hc, "plan_mode") {
require(
module_enabled(hc, "permissions.rules") || module_enabled(hc, "permissions.sandbox"),
"plan_mode",
"permissions.rules or permissions.sandbox",
)?;
}
if module_setting_bool(hc, "mcp", "serve") {
require(module_enabled(hc, "mcp"), "mcp (serve)", "mcp")?;
}
if module_enabled(hc, "tools_question") {
require(
module_enabled(hc, "tui") || module_enabled(hc, "server"),
"tools_question",
"tui or server",
)?;
}
if hc.core.skills.enabled == Some(true) {
let has_read = has("read_file");
let has_bash = has("bash");
if !has_read && !has_bash {
return Err(ResolveError::MissingDependency {
module: "core.skills".to_string(),
requires: "core.tools.enabled (read_file or bash)".to_string(),
});
}
if !has_read && has_bash {
warnings.push(
"D-7: core.skills is active with only `bash` as the read pathway (no \
dedicated read_file); progressive disclosure degrades to bash-only reads \
(§2.1 D-7, resolver warns rather than errors)"
.to_string(),
);
}
}
let span_summaries_on =
module_enabled(hc, "reduction") && module_setting_bool(hc, "reduction", "span_summaries");
let memory_on = module_enabled(hc, "memory");
if span_summaries_on || memory_on {
let small_model = module_setting_str(hc, "model_catalog", "small_model").unwrap_or("");
if small_model.is_empty() {
warnings.push(
"D-9: a small-model-consuming feature (reduction.span_summaries and/or \
memory) is enabled with no capabilities.model_catalog.small_model set — \
falls back to the main model (§2.1 D-9)"
.to_string(),
);
}
}
if module_enabled(hc, "tools_apply_patch") {
let co_advertised = has("edit_file") || has("write_file");
let per_model = module_setting_bool(hc, "tools_apply_patch", "per_model");
let model_catalog_on = module_enabled(hc, "model_catalog");
if co_advertised && !(per_model && model_catalog_on) {
warnings.push(
"C1: capabilities.tools_apply_patch is advertised alongside edit_file/\
write_file with no model_catalog per-model capability bits — format \
confusion risk (§2.2 C1)"
.to_string(),
);
}
}
if effective_sandbox(hc) == SandboxPolicy::DangerFullAccess
&& effective_approval(hc) == ApprovalPolicy::Never
{
warnings.push(
"C3 (MANDATORY): capabilities.permissions resolves to \
sandbox=danger_full_access + approval=never — zero gates. Legal, but never \
safe-by-default; presets must never label this posture safe (§2.2 C3)"
.to_string(),
);
}
if let Some(baseline) = preset_baseline {
let approval_changed = permissions_approval_raw(hc) != permissions_approval_raw(baseline);
let prompt_changed = hc.core.system_prompt != baseline.core.system_prompt
|| hc.core.append_system_prompt != baseline.core.append_system_prompt;
if approval_changed != prompt_changed {
warnings.push(
"C4: capabilities.permissions.approval was overridden independently of \
core.system_prompt/append_system_prompt (or vice versa) — this preset pins \
the two together (§2.2 C4)"
.to_string(),
);
}
}
let bg_exposure = module_enabled(hc, "tools_background")
|| (module_enabled(hc, "subagents") && module_setting_bool(hc, "subagents", "background"));
if bg_exposure {
match module_setting_str(hc, "subagents", "background_prompts") {
Some("parent") | Some("auto_policy") => {}
_ => {
let approval_raw = permissions_approval_raw(hc).unwrap_or_default();
let is_model_requested = approval_raw
.replace('_', "-")
.eq_ignore_ascii_case("model-requested");
if is_model_requested {
warnings.push(
"C6 (S8 judgment call): background execution proceeds under \
approval=model_requested with no literal \
capabilities.subagents.background_prompts key — the model's own \
escalation is treated as the required auto-policy, not a literal \
schema-key match (§2.2 C6, §4.6 cx-parity residual)"
.to_string(),
);
} else {
return Err(ResolveError::Conflict {
name: "C6".to_string(),
detail: "tools_background and/or subagents.background is enabled \
without capabilities.subagents.background_prompts set to \
\"parent\" or \"auto_policy\" — a detached task cannot prompt \
(§2.2 C6)"
.to_string(),
});
}
}
}
}
if let Some(w) = validate_permissions_case_sensitivity(hc) {
warnings.push(w);
}
Ok(warnings)
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum StrictSandboxValue {
Bare(String),
Table(StrictSandboxTable),
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictSandboxTable {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
tier: Option<String>,
#[serde(default)]
network: Option<StrictNetworkTable>,
#[serde(default)]
escalation: Option<String>,
#[serde(default)]
env_policy: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictNetworkTable {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
allow_domains: Option<Vec<String>>,
#[serde(default)]
deny_domains: Option<Vec<String>>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictRulesTable {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
deny: Option<Vec<String>>,
#[serde(default)]
ask: Option<Vec<String>>,
#[serde(default)]
allow: Option<Vec<String>>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictProtectedPathsTable {
#[serde(default)]
enabled: Option<bool>,
#[serde(default)]
paths: Option<Vec<String>>,
}
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct StrictPermissionsSettings {
#[serde(default)]
approval: Option<String>,
#[serde(default)]
sandbox: Option<StrictSandboxValue>,
#[serde(default)]
auto_approved_tools: Option<Vec<String>>,
#[serde(default)]
rules: Option<StrictRulesTable>,
#[serde(default)]
protected_paths: Option<StrictProtectedPathsTable>,
}
fn validate_permissions_case_sensitivity(hc: &HarnessConfig) -> Option<String> {
let cap = hc.capabilities.get("permissions")?;
if cap.settings.is_empty() {
return None;
}
let value = serde_json::Value::Object(cap.settings.clone());
match serde_json::from_value::<StrictPermissionsSettings>(value) {
Ok(_) => None,
Err(e) => Some(format!(
"SECURITY: capabilities.permissions carries an unrecognized or case-mismatched \
key and was rejected by the strict, case-sensitive schema (a typo like `Tier`/\
`Sandbox` is never silently honored) — {e}"
)),
}
}
fn require(met: bool, module: &str, requires: &str) -> Result<(), ResolveError> {
if met {
Ok(())
} else {
Err(ResolveError::MissingDependency {
module: module.to_string(),
requires: requires.to_string(),
})
}
}
const MAX_EXTENDS_DEPTH: usize = 8;
const KNOWN_TOP_KEYS: &[&str] = &[
"schema_version",
"extends",
"core",
"capabilities",
"experimental",
];
const KNOWN_CORE_KEYS: &[&str] = &[
"model",
"base_url",
"api_key_env",
"api_key_cmd",
"effort",
"temperature",
"max_tokens",
"max_iterations",
"max_total_output_tokens",
"max_tool_output_bytes",
"parallel_tool_calls",
"shell_env_snapshot",
"system_prompt",
"append_system_prompt",
"project_context",
"env_context",
"context_injections",
"nested_instructions",
"instruction_imports",
"project_root_markers",
"hot_reload",
"additional_dirs",
"extra_headers",
"extra_body",
"model_switch",
"retry",
"tools",
"skills",
"prompts",
"compaction",
"session",
"steering",
"output",
];
fn unknown_keys(text: &str) -> Result<Vec<String>, HarnessConfigError> {
let value: toml::Value = toml::from_str(text).map_err(HarnessConfigError::Toml)?;
let mut out = Vec::new();
let Some(tbl) = value.as_table() else {
return Ok(out);
};
for k in tbl.keys() {
if !KNOWN_TOP_KEYS.contains(&k.as_str()) {
out.push(k.clone());
}
}
if let Some(core) = tbl.get("core").and_then(|v| v.as_table()) {
for k in core.keys() {
if !KNOWN_CORE_KEYS.contains(&k.as_str()) {
out.push(format!("core.{k}"));
}
}
}
if let Some(caps) = tbl.get("capabilities").and_then(|v| v.as_table()) {
for k in caps.keys() {
if !MODULE_NAMES.contains(&k.as_str()) {
out.push(format!("capabilities.{k}"));
}
}
}
Ok(out)
}
fn resolve_preset_chain(
name_or_path: &str,
allow_path: bool,
base_dir: Option<&std::path::Path>,
depth: usize,
seen: &mut Vec<String>,
) -> Result<HarnessConfig, ResolveError> {
if depth > MAX_EXTENDS_DEPTH {
return Err(ResolveError::DepthExceeded(seen.clone()));
}
if seen.iter().any(|s| s == name_or_path) {
let mut chain = seen.clone();
chain.push(name_or_path.to_string());
return Err(ResolveError::Cycle(chain));
}
seen.push(name_or_path.to_string());
let (hc, next_base_dir): (HarnessConfig, Option<std::path::PathBuf>) =
if let Some(toml_text) = crate::presets::lookup(name_or_path) {
(
HarnessConfig::from_toml_str(toml_text).map_err(ResolveError::Parse)?,
base_dir.map(std::path::Path::to_path_buf),
)
} else {
if !allow_path {
return Err(ResolveError::PathExtendsNotAllowed(
name_or_path.to_string(),
));
}
let path = match base_dir {
Some(dir) => dir.join(name_or_path),
None => std::path::PathBuf::from(name_or_path),
};
let text = std::fs::read_to_string(&path)
.map_err(|e| ResolveError::Io(path.clone(), e.to_string()))?;
let hc = HarnessConfig::from_toml_str(&text).map_err(ResolveError::Parse)?;
let dir = path.parent().map(std::path::Path::to_path_buf);
(hc, dir)
};
match hc.extends.clone() {
Some(parent_ref) => {
let parent = resolve_preset_chain(
&parent_ref,
allow_path,
next_base_dir.as_deref(),
depth + 1,
seen,
)?;
Ok(parent.overlay(&hc))
}
None => Ok(hc),
}
}
fn materialize_config(hc: &HarnessConfig) -> Config {
let mut profile = hc.to_config_profile();
if let Some(cap) = hc.capabilities.get("permissions") {
match cap.settings.get("sandbox") {
Some(serde_json::Value::String(s)) => profile.sandbox = Some(s.clone()),
Some(serde_json::Value::Object(o)) => {
if let Some(t) = o.get("tier").and_then(|v| v.as_str()) {
profile.sandbox = Some(t.to_string());
}
}
_ => {}
}
if let Some(a) = cap.settings.get("approval").and_then(|v| v.as_str()) {
profile.approval = Some(a.to_string());
}
if let Some(list) = cap
.settings
.get("auto_approved_tools")
.and_then(|v| v.as_array())
{
profile.auto_approved_tools = Some(
list.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect(),
);
}
let (deny, allow) = permissions_rules_patterns(cap);
if !deny.is_empty() {
profile.tool_deny_patterns = Some(deny);
}
if !allow.is_empty() {
profile.tool_allow_patterns = Some(allow);
}
}
if let Some(cap) = hc.capabilities.get("deferred_tools") {
if cap.enabled == Some(true) {
profile.tool_advertising = Some("deferred".to_string());
profile.tool_advertising_core = deferred_tools_core(cap);
}
}
if let Some(cap) = hc.capabilities.get("cache") {
if cap.enabled == Some(true) {
profile.cache_plan = cache_plan_str(cap);
}
}
let mut config = ConfigBuilder::default().apply_profile(&profile).build();
config.module_registry = experimental_flag(hc, "module_registry");
config.module_activation = crate::modules::ModuleActivation::from_harness(hc);
config.core_tools_enabled = effective_tools_enabled(hc);
config.skills_enabled = hc.core.skills.enabled.unwrap_or(false);
if let Some(cap) = hc.capabilities.get("reduction") {
let setting = |name: &str| cap.settings.get(name).and_then(|v| v.as_bool());
config.reduction_policy = crate::config::ReductionPolicySettings {
stale_reads: setting("stale_reads"),
diff_reads: setting("diff_reads"),
duplicates: setting("duplicates"),
tool_input_elision: setting("tool_input_elision"),
supersede: setting("supersede"),
normalize_output: setting("normalize_output"),
image_redaction: setting("image_redaction"),
span_summaries: setting("span_summaries"),
};
config.handoff_enabled = cap.enabled.unwrap_or(true) && setting("handoff").unwrap_or(true);
}
if let Some(cap) = hc.capabilities.get("permissions") {
config.permissions_enabled = cap.enabled.unwrap_or(false);
config.permissions_ask_patterns = permissions_rules_ask_patterns(cap);
config.permissions_protected_paths = permissions_protected_paths(cap);
config.network_policy = permissions_network_policy(cap);
config.sandbox_os_enabled = permissions_sandbox_os_enabled(cap);
config.sandbox_escalation = permissions_sandbox_escalation(cap);
config.sandbox_env_policy = permissions_sandbox_env_policy(cap);
}
if let Some(cap) = hc.capabilities.get("subagents") {
config.subagents_enabled = cap.enabled.unwrap_or(false);
config.subagents_max_depth = cap
.settings
.get("max_depth")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(2);
config.subagents_max_concurrent = cap
.settings
.get("max_concurrent")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(4);
config.subagents_background = module_setting_bool(hc, "subagents", "background");
config.subagents_background_prompts = cap
.settings
.get("background_prompts")
.and_then(serde_json::Value::as_str)
.and_then(crate::subagents::BackgroundPromptsPolicy::parse);
config.subagents_definitions = subagent_definitions(cap);
}
if let Some(cap) = hc.capabilities.get("tui") {
config.tui_enabled = cap.enabled.unwrap_or(false);
if let Some(theme) = cap
.settings
.get("theme")
.and_then(serde_json::Value::as_str)
{
config.tui_theme = theme.to_string();
}
config.tui_vim_mode = cap
.settings
.get("vim_mode")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
if let Some(keymap) = cap.settings.get("keymap").and_then(|v| v.as_object()) {
config.tui_keymap = keymap
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
}
}
if let Some(cap) = hc.capabilities.get("session_tree") {
config.session_tree_enabled = cap.enabled.unwrap_or(false);
config.session_tree_branch_summaries = cap
.settings
.get("branch_summaries")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
config.session_tree_labels = cap
.settings
.get("labels")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
}
if let Some(cap) = hc.capabilities.get("tools_background") {
config.tools_background_enabled = cap.enabled.unwrap_or(false);
config.tools_background_max_concurrent = cap
.settings
.get("max_concurrent")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(crate::background::DEFAULT_MAX_CONCURRENT);
config.tools_background_max_output_bytes = cap
.settings
.get("max_output_bytes")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(crate::background::DEFAULT_MAX_OUTPUT_BYTES);
}
if let Some(cap) = hc.capabilities.get("checkpoint") {
config.checkpoint_enabled = cap.enabled.unwrap_or(false);
config.checkpoint_retain = cap
.settings
.get("retain")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(crate::checkpoint::DEFAULT_RETAIN);
}
if let Some(cap) = hc.capabilities.get("lsp") {
config.lsp_enabled = cap.enabled.unwrap_or(false);
config.lsp_servers = lsp_servers_from_settings(&cap.settings);
config.lsp_max_diagnostics = cap
.settings
.get("max_diagnostics")
.and_then(serde_json::Value::as_u64)
.map(|n| n as usize)
.unwrap_or(crate::lsp::DEFAULT_LSP_MAX_DIAGNOSTICS);
config.lsp_timeout_secs = cap
.settings
.get("timeout_secs")
.and_then(serde_json::Value::as_u64)
.unwrap_or(crate::lsp::DEFAULT_LSP_TIMEOUT_SECS);
}
if let Some(cap) = hc.capabilities.get("formatters") {
config.formatters_enabled = cap.enabled.unwrap_or(false);
config.formatters_diff_back = cap
.settings
.get("diff_back")
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
config.formatters_timeout_secs = cap
.settings
.get("timeout_secs")
.and_then(serde_json::Value::as_u64)
.unwrap_or(crate::formatters::DEFAULT_FORMATTER_TIMEOUT_SECS);
config.formatters = formatters_from_settings(&cap.settings);
}
if let Some(cap) = hc.capabilities.get("trust") {
config.trust_enabled = cap.enabled.unwrap_or(false);
config.trust_default = cap
.settings
.get("default")
.and_then(serde_json::Value::as_str)
.and_then(crate::plugins::TrustDecision::parse)
.unwrap_or_default(); }
if let Some(cap) = hc.capabilities.get("plugins") {
config.plugins_enabled = cap.enabled.unwrap_or(false);
config.plugins_dirs = string_array(cap.settings.get("dirs"))
.into_iter()
.map(std::path::PathBuf::from)
.collect();
}
let mc = crate::model_catalog::resolve(&hc.capabilities, &config.model);
config.model = mc.model;
config.small_model = mc.small_model;
config.model_fallback = mc.fallback;
config
}
fn lsp_servers_from_settings(
settings: &serde_json::Map<String, serde_json::Value>,
) -> Vec<(String, crate::lsp::LspServerSpec)> {
let Some(servers) = settings.get("servers").and_then(|v| v.as_object()) else {
return Vec::new();
};
let mut names: Vec<&String> = servers.keys().collect();
names.sort();
names
.into_iter()
.filter_map(|name| {
let def = servers.get(name)?.as_object()?;
let command = def.get("command")?.as_str()?.to_string();
let args = string_array(def.get("args"));
let extensions = string_array(def.get("extensions"));
Some((
name.clone(),
crate::lsp::LspServerSpec {
command,
args,
extensions,
},
))
})
.collect()
}
fn formatters_from_settings(
settings: &serde_json::Map<String, serde_json::Value>,
) -> Vec<(String, crate::formatters::FormatterSpec)> {
const RESERVED: &[&str] = &["diff_back", "timeout_secs"];
let mut names: Vec<&String> = settings
.keys()
.filter(|k| !RESERVED.contains(&k.as_str()))
.collect();
names.sort();
names
.into_iter()
.filter_map(|name| {
let def = settings.get(name)?.as_object()?;
let command = def.get("command")?.as_str()?.to_string();
let args = string_array(def.get("args"));
let extensions = string_array(def.get("extensions"));
Some((
name.clone(),
crate::formatters::FormatterSpec {
command,
args,
extensions,
},
))
})
.collect()
}
fn string_array(v: Option<&serde_json::Value>) -> Vec<String> {
v.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
pub fn permissions_rules_patterns(cap: &CapabilityConfig) -> (Vec<String>, Vec<String>) {
let Some(rules) = cap.settings.get("rules").and_then(|v| v.as_object()) else {
return (Vec::new(), Vec::new());
};
let deny = rules
.get("deny")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let allow = rules
.get("allow")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
(deny, allow)
}
pub fn permissions_rules_ask_patterns(cap: &CapabilityConfig) -> Vec<String> {
cap.settings
.get("rules")
.and_then(|v| v.as_object())
.and_then(|rules| rules.get("ask"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
pub fn permissions_protected_paths(cap: &CapabilityConfig) -> Vec<String> {
cap.settings
.get("protected_paths")
.and_then(|v| v.as_object())
.and_then(|pp| pp.get("paths"))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
pub fn subagent_definitions(
cap: &CapabilityConfig,
) -> std::collections::HashMap<String, crate::subagents::NamedAgentDefinition> {
let mut out = std::collections::HashMap::new();
let Some(agents) = cap.settings.get("agents").and_then(|v| v.as_object()) else {
return out;
};
for (name, def) in agents {
let Some(obj) = def.as_object() else { continue };
let system_prompt = obj
.get("system_prompt")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let tools = obj.get("tools").and_then(|v| v.as_array()).map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect::<Vec<_>>()
});
let model = obj.get("model").and_then(|v| v.as_str()).map(String::from);
out.insert(
name.clone(),
crate::subagents::NamedAgentDefinition {
name: name.clone(),
system_prompt,
tools,
model,
},
);
}
out
}
pub fn permissions_network_policy(cap: &CapabilityConfig) -> Option<crate::tools::NetworkPolicy> {
let sandbox = cap.settings.get("sandbox")?.as_object()?;
let network = sandbox.get("network").and_then(|v| v.as_object());
let enabled = network
.and_then(|n| n.get("enabled"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let string_list = |key: &str| -> Vec<String> {
network
.and_then(|n| n.get(key))
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
};
Some(crate::tools::NetworkPolicy {
enabled,
allow_domains: string_list("allow_domains"),
deny_domains: string_list("deny_domains"),
})
}
pub fn permissions_sandbox_os_enabled(cap: &CapabilityConfig) -> Option<bool> {
cap.settings
.get("sandbox")?
.as_object()?
.get("enabled")?
.as_bool()
}
pub fn permissions_sandbox_escalation(cap: &CapabilityConfig) -> crate::sandbox::SandboxEscalation {
cap.settings
.get("sandbox")
.and_then(|v| v.as_object())
.and_then(|o| o.get("escalation"))
.and_then(|v| v.as_str())
.and_then(crate::sandbox::SandboxEscalation::parse)
.unwrap_or_default()
}
pub fn permissions_sandbox_env_policy(cap: &CapabilityConfig) -> crate::sandbox::SandboxEnvPolicy {
cap.settings
.get("sandbox")
.and_then(|v| v.as_object())
.and_then(|o| o.get("env_policy"))
.and_then(|v| v.as_str())
.and_then(crate::sandbox::SandboxEnvPolicy::parse)
.unwrap_or_default()
}
pub fn deferred_tools_core(cap: &CapabilityConfig) -> Option<Vec<String>> {
cap.settings
.get("core")
.and_then(|v| v.as_array())
.map(|list| {
list.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
}
pub fn cache_plan_str(cap: &CapabilityConfig) -> Option<String> {
cap.settings
.get("plan")
.and_then(|v| v.as_str())
.map(String::from)
}
pub fn server_bind(cap: &CapabilityConfig) -> Option<String> {
cap.settings
.get("bind")
.and_then(|v| v.as_str())
.map(String::from)
}
pub fn server_token(cap: &CapabilityConfig) -> Option<String> {
cap.settings
.get("token")
.and_then(|v| v.as_str())
.map(String::from)
}
fn experimental_flag(hc: &HarnessConfig, key: &str) -> bool {
hc.experimental
.get(key)
.and_then(|v| v.as_bool())
.unwrap_or(false)
}
pub struct Resolved {
pub config: Config,
pub harness: HarnessConfig,
pub modules: BTreeMap<String, bool>,
pub preset_chain: Vec<String>,
pub warnings: Vec<String>,
}
impl std::fmt::Debug for Resolved {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Resolved")
.field("config", &"<Config, not Debug>")
.field("harness", &self.harness)
.field("modules", &self.modules)
.field("preset_chain", &self.preset_chain)
.field("warnings", &self.warnings)
.finish()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ResolveOptions {
pub strict: bool,
}
#[derive(Debug)]
pub enum ResolveError {
Parse(HarnessConfigError),
Cycle(Vec<String>),
DepthExceeded(Vec<String>),
PathExtendsNotAllowed(String),
Io(std::path::PathBuf, String),
UnknownKey(String),
MissingDependency {
module: String,
requires: String,
},
Conflict {
name: String,
detail: String,
},
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::Parse(e) => write!(f, "{e}"),
ResolveError::Cycle(chain) => {
write!(f, "extends cycle detected: {}", chain.join(" -> "))
}
ResolveError::DepthExceeded(chain) => write!(
f,
"extends chain exceeds the depth-8 cap (§3.5 step 2): {}",
chain.join(" -> ")
),
ResolveError::PathExtendsNotAllowed(p) => write!(
f,
"extends = \"{p}\" names a path, which is only legal at the user/global layer \
(§3.3: a project file may never `extends` a path)"
),
ResolveError::Io(path, e) => write!(f, "failed to read {}: {e}", path.display()),
ResolveError::UnknownKey(k) => write!(
f,
"unknown key `{k}` under schema_version = 1 (strict mode, §3.5 step 5)"
),
ResolveError::MissingDependency { module, requires } => write!(
f,
"{module} is enabled but its hard dependency is unmet: requires {requires} \
(§2.1, §3.5 step 6)"
),
ResolveError::Conflict { name, detail } => write!(f, "{name}: {detail}"),
}
}
}
impl std::error::Error for ResolveError {}
pub fn resolve(
top_toml: &str,
project_toml: Option<&str>,
opts: &ResolveOptions,
) -> Result<Resolved, ResolveError> {
let mut warnings = Vec::new();
let top_unknown = unknown_keys(top_toml).map_err(ResolveError::Parse)?;
if opts.strict {
if let Some(first) = top_unknown.first() {
return Err(ResolveError::UnknownKey(first.clone()));
}
} else {
for k in &top_unknown {
warnings.push(format!(
"unknown key `{k}` (lenient mode; would error under --strict-config, §3.5 step 5)"
));
}
}
let top = HarnessConfig::from_toml_str(top_toml).map_err(ResolveError::Parse)?;
resolve_top(top, project_toml, opts, warnings)
}
pub fn resolve_harness(
top: HarnessConfig,
opts: &ResolveOptions,
) -> Result<Resolved, ResolveError> {
resolve_top(top, None, opts, Vec::new())
}
fn resolve_top(
top: HarnessConfig,
project_toml: Option<&str>,
opts: &ResolveOptions,
mut warnings: Vec<String>,
) -> Result<Resolved, ResolveError> {
let mut preset_chain_names = Vec::new();
let preset_layer = match &top.extends {
Some(ext) => Some(resolve_preset_chain(
ext,
true,
None,
0,
&mut preset_chain_names,
)?),
None => None,
};
preset_chain_names.reverse();
let mut top_no_extends = top.clone();
top_no_extends.extends = None;
let user_layer = match &preset_layer {
Some(pl) => pl.overlay(&top_no_extends),
None => top_no_extends,
};
let final_hc = match project_toml {
Some(proj_text) => {
let proj_unknown = unknown_keys(proj_text).map_err(ResolveError::Parse)?;
if opts.strict {
if let Some(first) = proj_unknown.first() {
return Err(ResolveError::UnknownKey(first.clone()));
}
} else {
for k in &proj_unknown {
warnings.push(format!(
"unknown key `{k}` in project config (lenient mode, §3.5 step 5)"
));
}
}
let proj = HarnessConfig::from_toml_str(proj_text).map_err(ResolveError::Parse)?;
let (sanitized, dropped) = sanitize_for_project(&proj);
for d in &dropped {
warnings.push(format!(
"project config: dropped untrusted key `{d}` (§3.3 monotonic tightening)"
));
}
let mut merged = user_layer.overlay(&sanitized);
match merge_permissions_capability(
user_layer.capabilities.get("permissions"),
sanitized.capabilities.get("permissions"),
) {
Some(mp) => {
merged.capabilities.insert("permissions".to_string(), mp);
}
None => {
merged.capabilities.remove("permissions");
}
}
match merge_reduction_capability(
user_layer.capabilities.get("reduction"),
sanitized.capabilities.get("reduction"),
) {
Some(reduction) => {
merged
.capabilities
.insert("reduction".to_string(), reduction);
}
None => {
merged.capabilities.remove("reduction");
}
}
let clamped = clamp_project_permissions(&user_layer, &sanitized, &mut merged);
for c in &clamped {
warnings.push(format!(
"project config: clamped `{c}` to the stricter base-layer value \
(§3.3 monotonic tightening)"
));
}
merged
}
None => user_layer,
};
for name in final_hc.capabilities.keys() {
if !MODULE_NAMES.contains(&name.as_str()) {
let path = format!("capabilities.{name}");
if !warnings.iter().any(|warning| warning.contains(&path)) {
warnings.push(format!(
"unknown capability module `{path}` (lenient mode; ignored)"
));
}
}
}
let module_warnings = validate_modules(&final_hc, preset_layer.as_ref())?;
warnings.extend(module_warnings);
let config = materialize_config(&final_hc);
let modules = activation_set(&final_hc);
Ok(Resolved {
config,
harness: final_hc,
modules,
preset_chain: preset_chain_names,
warnings,
})
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_TOML: &str = r#"
schema_version = 1
extends = "pi-core"
[core]
model = "anthropic/claude-opus-4-8"
effort = "high"
max_iterations = 40
project_context = true
[core.compaction]
after_messages = 50
reserve_tokens = 24000
[core.tools]
enabled = ["read_file", "bash", "edit_file", "write_file"]
schema_tier = "medium"
[core.tools.bash]
enabled = true
timeout_secs = 120
[capabilities.todos]
enabled = true
[capabilities.reduction]
enabled = true
span_summaries = true
[experimental]
some_staged_flag = true
"#;
#[test]
fn harness_config_parses_the_annotated_schema() {
let hc = HarnessConfig::from_toml_str(SAMPLE_TOML).expect("parses");
assert_eq!(hc.schema_version, 1);
assert_eq!(hc.extends.as_deref(), Some("pi-core"));
assert_eq!(hc.core.model.as_deref(), Some("anthropic/claude-opus-4-8"));
assert_eq!(hc.core.effort.as_deref(), Some("high"));
assert_eq!(hc.core.max_iterations, Some(40));
assert_eq!(hc.core.compaction.after_messages, Some(50));
assert_eq!(hc.core.compaction.reserve_tokens, Some(24000));
assert_eq!(hc.core.tools.schema_tier.as_deref(), Some("medium"));
assert_eq!(hc.core.tools.bash.enabled, Some(true));
assert_eq!(hc.core.tools.bash.timeout_secs, Some(120));
assert_eq!(hc.capabilities["todos"].enabled, Some(true));
assert_eq!(hc.capabilities["reduction"].enabled, Some(true));
assert_eq!(
hc.capabilities["reduction"].settings.get("span_summaries"),
Some(&serde_json::Value::Bool(true))
);
assert_eq!(
hc.experimental.get("some_staged_flag"),
Some(&serde_json::Value::Bool(true))
);
}
#[test]
fn harness_config_resolves_core_into_a_real_config() {
let hc = HarnessConfig::from_toml_str(SAMPLE_TOML).expect("parses");
let config = hc.resolve_core();
assert_eq!(config.model, "anthropic/claude-opus-4-8");
assert_eq!(config.effort.as_deref(), Some("high"));
assert_eq!(config.max_iterations, 40);
assert_eq!(config.compact_after_messages, Some(50));
assert_eq!(config.tool_schema_tier, crate::tools::SchemaTier::Medium);
assert!(config.tool_enabled("bash"));
}
#[test]
fn harness_config_json_mirror_round_trips_the_same_shape() {
let toml_parsed = HarnessConfig::from_toml_str(SAMPLE_TOML).expect("toml parses");
let json_text = serde_json::to_string(&toml_parsed).expect("serializes to json");
let json_parsed = HarnessConfig::from_json_str(&json_text).expect("json parses back");
assert_eq!(
json_parsed, toml_parsed,
"TOML- and JSON-parsed HarnessConfig must be structurally identical"
);
}
#[test]
fn harness_config_rejects_unknown_schema_version() {
let err = HarnessConfig::from_toml_str("schema_version = 2\n")
.expect_err("schema_version 2 must be rejected");
assert!(matches!(
err,
HarnessConfigError::UnsupportedSchemaVersion(2)
));
let err = HarnessConfig::from_json_str(r#"{"schema_version": 2}"#)
.expect_err("schema_version 2 must be rejected (json)");
assert!(matches!(
err,
HarnessConfigError::UnsupportedSchemaVersion(2)
));
assert!(HarnessConfig::from_toml_str("schema_version = 1\n").is_ok());
assert!(HarnessConfig::from_toml_str("").is_ok());
}
#[test]
fn absent_core_table_defaults_the_whole_region() {
let hc = HarnessConfig::from_toml_str("schema_version = 1\n").expect("parses");
assert_eq!(hc.core, CoreSection::default());
}
#[test]
fn extends_parses_as_a_stub_not_yet_resolved() {
let hc = HarnessConfig::from_toml_str(
r#"
extends = "cc-parity"
[core]
model = "x"
"#,
)
.expect("parses");
assert_eq!(hc.extends.as_deref(), Some("cc-parity"));
let config = hc.resolve_core();
assert_eq!(config.model, "x");
}
#[test]
fn expand_env_vars_no_placeholder_is_unchanged() {
assert_eq!(
expand_env_vars("https://openrouter.ai/api/v1"),
"https://openrouter.ai/api/v1"
);
assert_eq!(expand_env_vars(""), "");
}
#[test]
fn expand_env_vars_substitutes_set_variables() {
std::env::set_var("SUPERCODE_TEST_ENV_EXPAND_HOST", "my-proxy.example");
std::env::set_var("SUPERCODE_TEST_ENV_EXPAND_PORT", "8080");
assert_eq!(
expand_env_vars(
"https://${SUPERCODE_TEST_ENV_EXPAND_HOST}:${SUPERCODE_TEST_ENV_EXPAND_PORT}/v1"
),
"https://my-proxy.example:8080/v1"
);
std::env::remove_var("SUPERCODE_TEST_ENV_EXPAND_HOST");
std::env::remove_var("SUPERCODE_TEST_ENV_EXPAND_PORT");
}
#[test]
fn expand_env_vars_unset_variable_stays_literal() {
assert_eq!(
expand_env_vars("token=${SUPERCODE_TEST_DEFINITELY_UNSET_VAR_XYZ}"),
"token=${SUPERCODE_TEST_DEFINITELY_UNSET_VAR_XYZ}"
);
}
#[test]
fn expand_env_vars_unterminated_brace_is_literal_and_safe() {
assert_eq!(expand_env_vars("prefix ${OOPS"), "prefix ${OOPS");
}
#[test]
fn to_config_profile_expands_env_vars_in_base_url_and_system_prompt() {
std::env::set_var("SUPERCODE_TEST_ENV_EXPAND_ENDPOINT", "vendor.example/v1");
let hc = HarnessConfig::from_toml_str(
r#"
schema_version = 1
[core]
base_url = "https://${SUPERCODE_TEST_ENV_EXPAND_ENDPOINT}"
system_prompt = "You are deployed at ${SUPERCODE_TEST_ENV_EXPAND_ENDPOINT}."
"#,
)
.expect("parses");
let config = hc.resolve_core();
assert_eq!(config.base_url, "https://vendor.example/v1");
assert_eq!(
config.system_prompt,
"You are deployed at vendor.example/v1."
);
std::env::remove_var("SUPERCODE_TEST_ENV_EXPAND_ENDPOINT");
}
#[test]
fn to_config_profile_does_not_expand_api_key_cmd() {
std::env::set_var("SUPERCODE_TEST_ENV_EXPAND_TOKEN", "should-not-appear");
let hc = HarnessConfig::from_toml_str(
r#"
schema_version = 1
[core]
api_key_cmd = "echo ${SUPERCODE_TEST_ENV_EXPAND_TOKEN}"
"#,
)
.expect("parses");
let config = hc.resolve_core();
assert_eq!(
config.api_key_cmd.as_deref(),
Some("echo ${SUPERCODE_TEST_ENV_EXPAND_TOKEN}")
);
std::env::remove_var("SUPERCODE_TEST_ENV_EXPAND_TOKEN");
}
#[test]
fn resolve_p4c_defaults_are_unset() {
let resolved =
resolve("schema_version = 1\n", None, &ResolveOptions::default()).expect("resolves");
assert!(!resolved.config.read_file_multimodal);
assert!(!resolved.config.edit_file_require_read_before_edit);
assert!(!resolved.config.edit_file_notebook_aware);
assert!(!resolved.config.shell_env_snapshot);
assert_eq!(resolved.config.doom_loop_threshold, None);
assert!(!resolved.config.nested_instructions);
assert!(!resolved.config.model_switch_allow_switch);
}
#[test]
fn resolve_applies_every_p4c_core_key() {
let toml = r#"
schema_version = 1
[core]
shell_env_snapshot = true
doom_loop_threshold = 4
nested_instructions = true
[core.tools.read_file]
multimodal = true
[core.tools.edit_file]
require_read_before_edit = true
notebook_aware = true
[core.model_switch]
allow_switch = true
"#;
let resolved = resolve(toml, None, &ResolveOptions::default()).expect("resolves");
assert!(resolved.config.read_file_multimodal);
assert!(resolved.config.edit_file_require_read_before_edit);
assert!(resolved.config.edit_file_notebook_aware);
assert!(resolved.config.shell_env_snapshot);
assert_eq!(resolved.config.doom_loop_threshold, Some(4));
assert!(resolved.config.nested_instructions);
assert!(resolved.config.model_switch_allow_switch);
}
#[test]
fn resolve_p4c_keys_survive_an_empty_project_layer() {
let top = r#"
schema_version = 1
[core]
doom_loop_threshold = 2
[core.tools.read_file]
multimodal = true
"#;
let resolved = resolve(
top,
Some("schema_version = 1\n"),
&ResolveOptions::default(),
)
.expect("resolves");
assert_eq!(resolved.config.doom_loop_threshold, Some(2));
assert!(resolved.config.read_file_multimodal);
}
#[test]
fn resolve_strips_core_session_operational_keys_from_a_project_layer() {
let top = r#"
schema_version = 1
[core.session]
dir = "/home/user/.trusted-sessions"
"#;
let project = r#"
schema_version = 1
[core.session]
dir = "/tmp/evil"
name = "attacker-named"
persist = false
retention_days = 0
export_format = "html"
git_metadata = true
auto_title = true
"#;
let resolved = resolve(top, Some(project), &ResolveOptions::default()).expect("resolves");
assert_eq!(
resolved.config.session_dir.as_deref(),
Some("/home/user/.trusted-sessions")
);
assert_eq!(resolved.config.session_name, None);
assert!(resolved.config.session_persist); assert_eq!(resolved.config.session_retention_days, None);
assert_eq!(
resolved.config.session_export_format,
crate::human_export::HumanExportFormat::Text
);
assert!(!resolved.config.session_git_metadata);
assert!(resolved.config.auto_title);
for key in [
"core.session.dir",
"core.session.name",
"core.session.persist",
"core.session.retention_days",
"core.session.export_format",
"core.session.git_metadata",
] {
assert!(
resolved.warnings.iter().any(|w| w.contains(key)),
"expected a dropped-key warning for `{key}`; warnings: {:?}",
resolved.warnings
);
}
assert!(
!resolved
.warnings
.iter()
.any(|w| w.contains("core.session.auto_title")),
"auto_title should NOT be dropped from a project layer: {:?}",
resolved.warnings
);
}
#[test]
fn resolve_user_layer_session_config_is_unaffected_by_project_stripping() {
let top = r#"
schema_version = 1
[core.session]
dir = "/home/user/.sessions"
name = "my-session"
persist = false
retention_days = 30
export_format = "html"
git_metadata = true
"#;
let resolved = resolve(top, None, &ResolveOptions::default()).expect("resolves");
assert_eq!(
resolved.config.session_dir.as_deref(),
Some("/home/user/.sessions")
);
assert_eq!(resolved.config.session_name.as_deref(), Some("my-session"));
assert!(!resolved.config.session_persist);
assert_eq!(resolved.config.session_retention_days, Some(30));
assert_eq!(
resolved.config.session_export_format,
crate::human_export::HumanExportFormat::Html
);
assert!(resolved.config.session_git_metadata);
}
}