feat: b5-skip-perms-flag — batch 5 upstream parity

This commit is contained in:
YeonGyu-Kim
2026-04-07 14:51:12 +09:00
parent d089d1a9cc
commit d509f16b5a
3 changed files with 444 additions and 109 deletions

View File

@@ -61,6 +61,16 @@ pub struct RuntimeFeatureConfig {
permission_mode: Option<ResolvedPermissionMode>,
permission_rules: RuntimePermissionRuleConfig,
sandbox: SandboxConfig,
provider_fallbacks: ProviderFallbackConfig,
}
/// Ordered chain of fallback model identifiers used when the primary
/// provider returns a retryable failure (429/500/503/etc.). The chain is
/// strict: each entry is tried in order until one succeeds.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProviderFallbackConfig {
primary: Option<String>,
fallbacks: Vec<String>,
}
/// Hook command lists grouped by lifecycle stage.
@@ -283,6 +293,7 @@ impl ConfigLoader {
permission_mode: parse_optional_permission_mode(&merged_value)?,
permission_rules: parse_optional_permission_rules(&merged_value)?,
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
};
Ok(RuntimeConfig {
@@ -367,6 +378,11 @@ impl RuntimeConfig {
pub fn sandbox(&self) -> &SandboxConfig {
&self.feature_config.sandbox
}
#[must_use]
pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig {
&self.feature_config.provider_fallbacks
}
}
impl RuntimeFeatureConfig {
@@ -421,6 +437,33 @@ impl RuntimeFeatureConfig {
pub fn sandbox(&self) -> &SandboxConfig {
&self.sandbox
}
#[must_use]
pub fn provider_fallbacks(&self) -> &ProviderFallbackConfig {
&self.provider_fallbacks
}
}
impl ProviderFallbackConfig {
#[must_use]
pub fn new(primary: Option<String>, fallbacks: Vec<String>) -> Self {
Self { primary, fallbacks }
}
#[must_use]
pub fn primary(&self) -> Option<&str> {
self.primary.as_deref()
}
#[must_use]
pub fn fallbacks(&self) -> &[String] {
&self.fallbacks
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.fallbacks.is_empty()
}
}
impl RuntimePluginConfig {
@@ -776,6 +819,23 @@ fn parse_optional_sandbox_config(root: &JsonValue) -> Result<SandboxConfig, Conf
})
}
fn parse_optional_provider_fallbacks(
root: &JsonValue,
) -> Result<ProviderFallbackConfig, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(ProviderFallbackConfig::default());
};
let Some(value) = object.get("providerFallbacks") else {
return Ok(ProviderFallbackConfig::default());
};
let entry = expect_object(value, "merged settings.providerFallbacks")?;
let primary =
optional_string(entry, "primary", "merged settings.providerFallbacks")?.map(str::to_string);
let fallbacks = optional_string_array(entry, "fallbacks", "merged settings.providerFallbacks")?
.unwrap_or_default();
Ok(ProviderFallbackConfig { primary, fallbacks })
}
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
match value {
"off" => Ok(FilesystemIsolationMode::Off),
@@ -1247,6 +1307,66 @@ mod tests {
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn parses_provider_fallbacks_chain_with_primary_and_ordered_fallbacks() {
// given
let root = temp_dir();
let cwd = root.join("project");
let home = root.join("home").join(".claw");
fs::create_dir_all(cwd.join(".claw")).expect("project config dir");
fs::create_dir_all(&home).expect("home config dir");
fs::write(
home.join("settings.json"),
r#"{
"providerFallbacks": {
"primary": "claude-opus-4-6",
"fallbacks": ["grok-3", "grok-3-mini"]
}
}"#,
)
.expect("write provider fallback settings");
// when
let loaded = ConfigLoader::new(&cwd, &home)
.load()
.expect("config should load");
// then
let chain = loaded.provider_fallbacks();
assert_eq!(chain.primary(), Some("claude-opus-4-6"));
assert_eq!(
chain.fallbacks(),
&["grok-3".to_string(), "grok-3-mini".to_string()]
);
assert!(!chain.is_empty());
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn provider_fallbacks_default_is_empty_when_unset() {
// given
let root = temp_dir();
let cwd = root.join("project");
let home = root.join("home").join(".claw");
fs::create_dir_all(&home).expect("home config dir");
fs::create_dir_all(&cwd).expect("project dir");
fs::write(home.join("settings.json"), "{}").expect("write empty settings");
// when
let loaded = ConfigLoader::new(&cwd, &home)
.load()
.expect("config should load");
// then
let chain = loaded.provider_fallbacks();
assert_eq!(chain.primary(), None);
assert!(chain.fallbacks().is_empty());
assert!(chain.is_empty());
fs::remove_dir_all(root).expect("cleanup temp dir");
}
#[test]
fn parses_typed_mcp_and_oauth_config() {
let root = temp_dir();

View File

@@ -57,8 +57,8 @@ pub use config::{
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig,
RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
CLAW_SETTINGS_SCHEMA_NAME,
};
pub use conversation::{