mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
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:
1 parent
9feba1fac9
commit
9e83636bc9
9 files changed
+4
-276
No files matched your search
@@ -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)?;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -27,7 +27,7 @@ export const docsAuth: DocsAuthDict = {
|
||||
],
|
||||
storageTitle: "会话存放在哪里",
|
||||
storageLead:
|
||||
"账户会话优先使用操作系统的凭证管理器;在没有凭证管理器的环境(无头主机、SSH、容器)中自动回退到权限为 0600 的私有 Codewhale 密钥文件。旧的 {fileStoreEnv} 显式开关已弃用并被忽略。",
|
||||
"账户会话优先使用操作系统的凭证管理器;在没有凭证管理器的环境(无头主机、SSH、容器)中自动回退到权限为 0600 的私有 Codewhale 密钥文件。",
|
||||
vaultTitle: "账户自己的密钥库",
|
||||
vaultLead: "{keys} 管理已登录账户的自带密钥(BYOK)库,且永远不会显示密钥内容。",
|
||||
portableTitle: "迁移到另一台机器",
|
||||
|
||||
Reference in new issue
Block a user