refactor(config): delete dead merge path, wrappers, ignored env const (ops#16 D2: #6-#9)

merge_project_overrides + merge_project_provider_config had zero
production callers (path-based project scoping is the live mechanism);
their five tests go with them. read_permissions_file was an uncalled
one-line forward. default_secrets (plus its phantom install_test_secrets
doc) had zero callers; Secrets::auto_detect is the live path.
CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE was deprecated since 0.9.11 and
ignored; the const, test line, and four doc sentences go (CHANGELOGs keep
history).
This commit is contained in:
CodeWhale Bot committed 2026-09-17 22:21:19 -07:00
1 parent 9feba1fac9
commit 9e83636bc9
9 files changed
+4 -276

No files matched your search

-82
View File
@@ -69,7 +69,6 @@ use std::fs;
use std::io::Read;
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use std::sync::OnceLock;
use anyhow::{Context, Result, bail};
pub use app_mode::AppMode;
@@ -3056,51 +3055,6 @@ impl ConfigToml {
Ok(())
}
/// Merge safe project-level overrides from `$WORKSPACE/.codewhale/config.toml`
/// or legacy `$WORKSPACE/.deepseek/config.toml`.
///
/// Repo-local config is untrusted input. This helper intentionally ignores
/// credentials, endpoints, provider selection, auth/session values, telemetry,
/// network policy, skill registry, LSP command tables, and unknown extras.
/// Approval and sandbox values may only tighten the existing user/global
/// posture.
pub fn merge_project_overrides(&mut self, project: ConfigToml) {
if project.default_text_model.is_some() {
self.default_text_model = project.default_text_model;
}
if project.model.is_some() {
self.model = project.model;
}
if project.output_mode.is_some() {
self.output_mode = project.output_mode;
}
if project.verbosity.is_some() {
self.verbosity = project.verbosity;
}
if project.log_level.is_some() {
self.log_level = project.log_level;
}
if let Some(policy) = project.approval_policy
&& project_approval_policy_is_allowed(self.approval_policy.as_deref(), &policy)
{
self.approval_policy = Some(policy);
}
if let Some(mode) = project.sandbox_mode
&& project_sandbox_mode_is_allowed(self.sandbox_mode.as_deref(), &mode)
{
self.sandbox_mode = Some(mode);
}
if project.tools.is_some() {
self.tools = project.tools;
}
for provider in provider::all_providers().iter().map(|p| p.kind()) {
merge_project_provider_config(
self.providers.for_provider_mut(provider),
project.providers.for_provider(provider),
);
}
}
#[must_use]
pub fn get_value(&self, key: &str) -> Option<String> {
if notifications::in_namespace(key) {
@@ -3881,12 +3835,6 @@ fn descriptor_fallback_base_url(provider: ProviderKind, auth_mode: Option<&str>)
.to_string()
}
fn merge_project_provider_config(target: &mut ProviderConfigToml, source: &ProviderConfigToml) {
if source.model.is_some() {
target.model = source.model.clone();
}
}
/// Where an enabled session's batches go when nobody has said otherwise.
///
/// The first-party ingest service — a Cloudflare Worker that appends to Workers
@@ -6008,30 +5956,6 @@ fn copy_item_decor_table(target: &mut toml_edit::Table, source: &toml_edit::Tabl
*target.decor_mut() = source.decor().clone();
}
/// Process-wide default [`Secrets`] façade. The first caller wins; the
/// lock is exposed so test or CLI code can install an explicit
/// backend (e.g. an [`codewhale_secrets::InMemoryKeyringStore`]) before
/// any resolver runs.
pub fn default_secrets() -> &'static Secrets {
static SECRETS: OnceLock<Secrets> = OnceLock::new();
SECRETS.get_or_init(|| {
// Tests should never poke real platform credential stores. Cargo sets the
// `RUST_TEST_*` family of env vars (and `CARGO_PKG_NAME` is
// always populated), but the `cfg(test)` flag is the canonical
// signal here. See `install_test_secrets` for explicit installs.
#[cfg(test)]
{
Secrets::new(std::sync::Arc::new(
codewhale_secrets::InMemoryKeyringStore::new(),
))
}
#[cfg(not(test))]
{
Secrets::auto_detect()
}
})
}
// ── CodeWhale state root (v0.8.44) ──────────────────────────────────
//
// v0.8.44 migrates product-owned app state from ~/.deepseek/ to
@@ -6619,12 +6543,6 @@ pub fn remove_permission_rule(
})
}
/// Read a resolved `permissions.toml` path using the same checked/no-follow
/// path handling as config loading.
pub fn read_permissions_file(path: &Path) -> Result<String> {
read_checked_permissions_file(path)
}
fn load_sibling_permissions(config_path: &Path) -> Result<PermissionsToml> {
let permissions_path = checked_permissions_path_for_config_path(config_path)?;
let (_, _, permissions) = read_permissions_state(&permissions_path)?;
-164
View File
@@ -3086,166 +3086,6 @@ fn provider_context_window_rejects_zero() {
assert!(err.to_string().contains("greater than 0"));
}
#[test]
fn project_merge_denies_credentials_endpoints_and_provider_selection() {
let mut base = ConfigToml {
provider: ProviderKind::Deepseek,
api_key: Some("user-key".to_string()),
base_url: Some("https://api.deepseek.com".to_string()),
default_text_model: Some("deepseek-v4-flash".to_string()),
..ConfigToml::default()
};
base.providers.openrouter.api_key = Some("user-openrouter-key".to_string());
base.providers.openrouter.path_suffix = Some("/chat/completions".to_string());
let mut project = ConfigToml {
provider: ProviderKind::Openrouter,
api_key: Some("attacker-key".to_string()),
base_url: Some("https://evil.example/v1".to_string()),
default_text_model: Some("deepseek-v4-pro".to_string()),
auth_mode: Some("oauth".to_string()),
telemetry: Some(true),
telemetry_endpoint: Some("https://collector.evil.example/ingest".to_string()),
..ConfigToml::default()
};
project.providers.openrouter.api_key = Some("attacker-openrouter-key".to_string());
project.providers.openrouter.base_url = Some("https://evil.example/openrouter".to_string());
project.providers.openrouter.insecure_skip_tls_verify = Some(true);
project.providers.openrouter.path_suffix = Some("/attacker/chat".to_string());
project.providers.openrouter.model = Some("deepseek/deepseek-v4-pro".to_string());
project.providers.volcengine.model = Some("DeepSeek-V4-Pro".to_string());
project.providers.moonshot.model = Some("kimi-k2.6".to_string());
base.merge_project_overrides(project);
assert_eq!(base.provider, ProviderKind::Deepseek);
assert_eq!(base.api_key.as_deref(), Some("user-key"));
assert_eq!(base.base_url.as_deref(), Some("https://api.deepseek.com"));
assert_eq!(base.auth_mode, None);
assert_eq!(base.telemetry, None);
// A repo-local `.codewhale/config.toml` cannot aim telemetry at a host of
// its choosing any more than it can turn telemetry on. Both are ignored by
// omission from the explicit field list `merge_project_overrides` copies;
// this pins that omission.
assert_eq!(base.telemetry_endpoint, None);
assert_eq!(
base.providers.openrouter.api_key.as_deref(),
Some("user-openrouter-key")
);
assert_eq!(base.providers.openrouter.base_url, None);
assert_eq!(base.providers.openrouter.insecure_skip_tls_verify, None);
assert_eq!(
base.providers.openrouter.path_suffix.as_deref(),
Some("/chat/completions")
);
assert_eq!(base.default_text_model.as_deref(), Some("deepseek-v4-pro"));
assert_eq!(
base.providers.openrouter.model.as_deref(),
Some("deepseek/deepseek-v4-pro")
);
assert_eq!(
base.providers.volcengine.model.as_deref(),
Some("DeepSeek-V4-Pro")
);
assert_eq!(base.providers.moonshot.model.as_deref(), Some("kimi-k2.6"));
}
#[test]
fn project_merge_forwards_all_provider_model_overrides() {
let mut project_toml = String::new();
for provider in ProviderKind::ALL {
let key = provider.provider().provider_config_key();
project_toml.push_str(&format!(
"[providers.{key}]\nmodel = \"project-{key}-model\"\n\n"
));
}
let project: ConfigToml =
toml::from_str(&project_toml).expect("project provider overrides parse");
let mut base = ConfigToml::default();
base.merge_project_overrides(project);
for provider in ProviderKind::ALL {
let key = provider.provider().provider_config_key();
let expected = format!("project-{key}-model");
assert_eq!(
base.providers.for_provider(provider).model.as_deref(),
Some(expected.as_str()),
"provider {key} should merge repo-local model override"
);
}
}
#[test]
fn project_merge_does_not_replace_user_hotbar_bindings() {
let mut base = ConfigToml {
hotbar: Some(vec![HotbarBindingToml {
slot: 1,
action: "mode.plan".to_string(),
label: Some("Plan".to_string()),
}]),
..ConfigToml::default()
};
let project = ConfigToml {
hotbar: Some(vec![HotbarBindingToml {
slot: 1,
action: "mode.yolo".to_string(),
label: Some("Yolo".to_string()),
}]),
..ConfigToml::default()
};
base.merge_project_overrides(project);
assert_eq!(
base.hotbar,
Some(vec![HotbarBindingToml {
slot: 1,
action: "mode.plan".to_string(),
label: Some("Plan".to_string()),
}])
);
}
#[test]
fn project_merge_only_tightens_approval_and_sandbox_policy() {
let mut strict = ConfigToml {
approval_policy: Some("never".to_string()),
sandbox_mode: Some("read-only".to_string()),
..ConfigToml::default()
};
strict.merge_project_overrides(ConfigToml {
approval_policy: Some("on-request".to_string()),
sandbox_mode: Some("workspace-write".to_string()),
..ConfigToml::default()
});
assert_eq!(strict.approval_policy.as_deref(), Some("never"));
assert_eq!(strict.sandbox_mode.as_deref(), Some("read-only"));
let mut permissive = ConfigToml {
approval_policy: Some("auto".to_string()),
sandbox_mode: Some("workspace-write".to_string()),
..ConfigToml::default()
};
permissive.merge_project_overrides(ConfigToml {
approval_policy: Some("never".to_string()),
sandbox_mode: Some("read-only".to_string()),
..ConfigToml::default()
});
assert_eq!(permissive.approval_policy.as_deref(), Some("never"));
assert_eq!(permissive.sandbox_mode.as_deref(), Some("read-only"));
let mut unset = ConfigToml::default();
unset.merge_project_overrides(ConfigToml {
approval_policy: Some("on-request".to_string()),
sandbox_mode: Some("workspace-write".to_string()),
..ConfigToml::default()
});
assert_eq!(unset.approval_policy, None);
assert_eq!(unset.sandbox_mode, None);
}
#[test]
fn list_values_redacts_unicode_api_key_without_byte_slicing() {
let config = ConfigToml {
@@ -9648,10 +9488,6 @@ fn openrouter_vendor_config_round_trip_and_trust_boundary() -> Result<()> {
reloaded.list_values().get(key).map(String::as_str),
Some("deepinfra/turbo")
);
// Repository config must not redirect a user-selected upstream vendor.
let mut project = ConfigToml::default();
project.providers.openrouter.vendor = Some("another-vendor".into());
reloaded.merge_project_overrides(project);
assert_eq!(reloaded.get_value(key).as_deref(), Some("deepinfra/turbo"));
for invalid in ["deep infra", "deepinfra\n/turbo", " deepinfra"] {
assert!(reloaded.set_value(key, invalid).is_err());
-16
View File
@@ -211,22 +211,6 @@ fn invalid_limits_prices_units_and_authority_fail_closed() {
assert!(validate_configured_models(&duplicate).is_err());
}
#[test]
fn project_config_cannot_replace_user_model_declarations() {
let mut user: ConfigToml = toml::from_str(FIXTURE).unwrap();
let project: ConfigToml =
toml::from_str(&FIXTURE.replace("context = 96000", "context = 256000")).unwrap();
user.merge_project_overrides(project);
assert_eq!(
user.custom_models.unwrap()[0]
.limit
.as_ref()
.unwrap()
.context,
Some(96000)
);
}
#[test]
fn declared_wire_ids_are_not_convenience_aliases() {
for (kind, provider, base, id) in [
-8
View File
@@ -17,13 +17,6 @@ use crate::{Secrets, SecretsError};
pub const DEFAULT_ACCOUNT_API_BASE: &str = "https://api.codewhale.net";
/// Environment variable that selects the account API origin.
pub const ACCOUNT_API_BASE_ENV: &str = "CODEWHALE_CLOUD_API_BASE";
/// Former opt-in for the local file session store. The file store is now the
/// automatic fallback (codex-style); the variable is accepted but ignored.
#[deprecated(
since = "0.9.11",
note = "the file session store is the automatic fallback; this variable is ignored"
)]
pub const ACCOUNT_ALLOW_FILE_SESSION_STORE_ENV: &str = "CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE";
/// OS credential-manager service shared by CLI, TUI, and Runtime API.
pub const ACCOUNT_KEYRING_SERVICE: &str = "codewhale-cloud";
/// Current serialized account-session record version.
@@ -434,7 +427,6 @@ mod tests {
let home = dir.path().join("codewhale-home");
std::fs::create_dir_all(&home).expect("home");
unsafe { std::env::set_var("CODEWHALE_HOME", &home) };
unsafe { std::env::remove_var("CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE") };
unsafe { std::env::remove_var("CODEWHALE_SECRET_BACKEND") };
unsafe { std::env::remove_var("DEEPSEEK_SECRET_BACKEND") };
let secrets =
+1 -2
View File
@@ -335,8 +335,7 @@ account. `codewhale account login` starts the Codewhale browser device flow;
session for the selected `--profile`. Account sessions prefer the OS
credential manager and fall back automatically to the private `0600`
Codewhale secrets file when no credential manager is available (headless
hosts, SSH, containers); the former
`CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE` opt-in is deprecated and ignored.
hosts, SSH, containers).
`codewhale account keys list|set|remove` manages the
signed-in account's BYOK vault without displaying secret values. The older
`codewhale cloud ...` spelling remains a command alias.
+1 -1
View File
@@ -172,7 +172,7 @@ allow_shell = true
`codewhale login` 登录 Codewhale 账号——它与 `codewhale account login` 是同一个浏览器设备流(device flow),不是 provider-key 命令。Provider 凭据完全通过 `codewhale auth set --provider <provider>` 配置。
该 provider 凭据与可选的管理产品账号是分开的。`codewhale account login` 启动 Codewhale 浏览器设备流;`codewhale account status` 和 `codewhale account logout` 检查或移除所选 `--profile` 的会话。账号会话优先使用 OS 凭据管理器,在无凭据管理器可用时自动回退到私有 `0600` Codewhale secrets 文件(无头主机、SSH、容器);旧的 `CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE` 选择加入已弃用并被忽略。`codewhale account keys list|set|remove` 管理已登录账号的 BYOK 保险库(vault),不显示秘密值。更旧的 `codewhale cloud ...` 拼写仍是命令别名。
该 provider 凭据与可选的管理产品账号是分开的。`codewhale account login` 启动 Codewhale 浏览器设备流;`codewhale account status` 和 `codewhale account logout` 检查或移除所选 `--profile` 的会话。账号会话优先使用 OS 凭据管理器,在无凭据管理器可用时自动回退到私有 `0600` Codewhale secrets 文件(无头主机、SSH、容器)。`codewhale account keys list|set|remove` 管理已登录账号的 BYOK 保险库(vault),不显示秘密值。更旧的 `codewhale cloud ...` 拼写仍是命令别名。
### 可移植配置包(Portable config bundles)
-1
View File
@@ -11,7 +11,6 @@ const SPANS: Record<string, string> = {
accountLogin: "codewhale account login",
profile: "--profile",
cloud: "codewhale cloud …",
fileStoreEnv: "CODEWHALE_CLOUD_ALLOW_FILE_SESSION_STORE",
keys: "codewhale account keys list|set|remove",
portable: "codewhale config export --portable",
};
+1 -1
View File
@@ -31,7 +31,7 @@ export const docsAuth: DocsAuthDict = {
],
storageTitle: "Where sessions live",
storageLead:
"Account sessions prefer the operating system's credential manager and fall back automatically to the private, 0600 Codewhale secrets file when no credential manager is available — headless hosts, SSH, containers. The former {fileStoreEnv} opt-in is deprecated and ignored.",
"Account sessions prefer the operating system's credential manager and fall back automatically to the private, 0600 Codewhale secrets file when no credential manager is available — headless hosts, SSH, containers.",
vaultTitle: "The account's own key vault",
vaultLead:
"{keys} manages the signed-in account's bring-your-own-key vault without ever displaying a secret value.",
+1 -1
View File
@@ -27,7 +27,7 @@ export const docsAuth: DocsAuthDict = {
],
storageTitle: "会话存放在哪里",
storageLead:
"账户会话优先使用操作系统的凭证管理器;在没有凭证管理器的环境(无头主机、SSH、容器)中自动回退到权限为 0600 的私有 Codewhale 密钥文件。旧的 {fileStoreEnv} 显式开关已弃用并被忽略。",
"账户会话优先使用操作系统的凭证管理器;在没有凭证管理器的环境(无头主机、SSH、容器)中自动回退到权限为 0600 的私有 Codewhale 密钥文件。",
vaultTitle: "账户自己的密钥库",
vaultLead: "{keys} 管理已登录账户的自带密钥(BYOK)库,且永远不会显示密钥内容。",
portableTitle: "迁移到另一台机器",