Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
79 KiB
JSON

{"repo": "ashishtyagi10/crew", "files": {"crates/crew-plugin/src/credentials.rs": {"aa0d810fa9fa46917d5b98a14b71f2436168d058": {"t": 1785745866.399412, "n": 9122, "h": "3643546285b3bcbc", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n // From the auth registry, so adding a provider row teaches the\n // credential store about it too — no second name table here.\n crate::broker::auth::registry::name_for_var(var)\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// Move the provider pin without touching any key.\n///\n/// Picking a model whose vendor crew can reach with a key the user already\n/// holds should just work — `pick_provider`'s fixed order would otherwise\n/// keep answering with whichever provider happens to rank first, and the user\n/// would have to know that `CREW_PROVIDER` exists to get the model they just\n/// chose. Storing the pin is how the choice survives to the next request.\npub fn save_pin(provider: &str) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_pin_at(&path, provider)\n}\n\n/// [`save_pin`] at an explicit path (the testable half).\npub fn save_pin_at(path: &Path, provider: &str) -> anyhow::Result<()> {\n let mut store = load_from(path);\n store.provider = Some(provider.to_string());\n let bytes = serde_json::to_vec_pretty(&store)?;\n if let Some(dir) = path.parent() {\n std::fs::create_dir_all(dir)?;\n }\n write_atomic(path, &bytes)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "2880203a0decf0e3fbf4cd44b92b8b919eca00a8": {"t": 1785757401.0837514, "n": 9122, "h": "3643546285b3bcbc", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n // From the auth registry, so adding a provider row teaches the\n // credential store about it too — no second name table here.\n crate::broker::auth::registry::name_for_var(var)\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// Move the provider pin without touching any key.\n///\n/// Picking a model whose vendor crew can reach with a key the user already\n/// holds should just work — `pick_provider`'s fixed order would otherwise\n/// keep answering with whichever provider happens to rank first, and the user\n/// would have to know that `CREW_PROVIDER` exists to get the model they just\n/// chose. Storing the pin is how the choice survives to the next request.\npub fn save_pin(provider: &str) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_pin_at(&path, provider)\n}\n\n/// [`save_pin`] at an explicit path (the testable half).\npub fn save_pin_at(path: &Path, provider: &str) -> anyhow::Result<()> {\n let mut store = load_from(path);\n store.provider = Some(provider.to_string());\n let bytes = serde_json::to_vec_pretty(&store)?;\n if let Some(dir) = path.parent() {\n std::fs::create_dir_all(dir)?;\n }\n write_atomic(path, &bytes)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "075ac63abf2893651cf1644f5a736c3652d16785": {"t": 1785757402.5808556, "n": 9316, "h": "007ed2b19fe752d5", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n // Everything else comes from the provider table, so adding a row\n // teaches the credential store about it too.\n other => crate::broker::direct_by_name_for_var(other).map(|d| d.name),\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// Move the provider pin without touching any key.\n///\n/// Picking a model whose vendor crew can reach with a key the user already\n/// holds should just work — `pick_provider`'s fixed order would otherwise\n/// keep answering with whichever provider happens to rank first, and the user\n/// would have to know that `CREW_PROVIDER` exists to get the model they just\n/// chose. Storing the pin is how the choice survives to the next request.\npub fn save_pin(provider: &str) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_pin_at(&path, provider)\n}\n\n/// [`save_pin`] at an explicit path (the testable half).\npub fn save_pin_at(path: &Path, provider: &str) -> anyhow::Result<()> {\n let mut store = load_from(path);\n store.provider = Some(provider.to_string());\n let bytes = serde_json::to_vec_pretty(&store)?;\n if let Some(dir) = path.parent() {\n std::fs::create_dir_all(dir)?;\n }\n write_atomic(path, &bytes)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "40db45882a555c92c350f861d677a662f8ac4b20": {"t": 1785757404.1075642, "n": 8289, "h": "7b5d862c424b6790", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n // Everything else comes from the provider table, so adding a row\n // teaches the credential store about it too.\n other => crate::broker::direct_by_name_for_var(other).map(|d| d.name),\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "a38c893567e25289a110b7f01c2b68e64464a98f": {"t": 1785757405.6370864, "n": 8243, "h": "7414be53ba68958e", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n // Everything else comes from the provider table, so adding a row\n // teaches the credential store about it too.\n other => crate::broker::direct_by_name_for_var(other).map(|d| d.name),\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "6427bc6e1818f56ae2ce55787d7880329871f279": {"t": 1785757406.9138837, "n": 7851, "h": "d62e5b3fce71450d", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\npub const VARS: [&str; 3] = [\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n _ => None,\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "c393aaeab239d388c99b9fab865f71baec49c3b2": {"t": 1785757409.7437418, "n": 6319, "h": "05b67e6a6e46e646", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\npub const VARS: [&str; 3] = [\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n _ => None,\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\nfn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n let tmp = path.with_extension(\"json.tmp\");\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "a88056f84de19e9bac9458172607734f1a6ad212": {"t": 1785757410.9667974, "n": 5211, "h": "94b95169e618b64f", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\npub const VARS: [&str; 3] = [\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n _ => None,\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml`.\npub fn path() -> Option<PathBuf> {\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\nfn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n let tmp = path.with_extension(\"json.tmp\");\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "4b02e1540cc3435644fbe1c9c83aa326d96f643c": {"t": 1785757412.380803, "n": 4762, "h": "da9ea10531ccd6b1", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\npub const VARS: [&str; 3] = [\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n match var {\n \"DASHSCOPE_API_KEY\" => Some(\"dashscope\"),\n \"OPENROUTER_API_KEY\" => Some(\"openrouter\"),\n \"ANTHROPIC_API_KEY\" => Some(\"anthropic\"),\n _ => None,\n }\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml`.\npub fn path() -> Option<PathBuf> {\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\nfn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n let tmp = path.with_extension(\"json.tmp\");\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create(true).truncate(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}, "cc85ada37a079f5248726748eb0bab5b84758e55": {"t": 1786068330.0875366, "n": 9122, "h": "3643546285b3bcbc", "c": "//! Provider credentials supplied from inside crew, rather than from the\n//! environment. Stored as JSON next to `config.toml` — deliberately NOT inside\n//! `CrewConfig`, which is user-visible, hand-edited and safe to paste around;\n//! a key in there would leak the first time someone shared their config.\n//!\n//! Lives in `crew-plugin` because both consumers reach it here: the broker IS\n//! this crate, and `crew-app` already depends on it.\n//!\n//! Not the macOS Keychain: crew ships Linux binaries too, so Keychain would\n//! mean two code paths and a platform-specific failure mode for a v1. An\n//! owner-only file is what `gh` and `aws` do. Keychain stays open as an upgrade.\nuse std::collections::BTreeMap;\nuse std::path::{Path, PathBuf};\n\n/// The only variables this store will hold, so a key typed into the UI can\n/// never name an arbitrary environment variable.\n/// Kept as a slice, not a fixed array: providers are a table now\n/// (`broker::discover::DIRECT`), and a length in the type would have to be\n/// hand-edited every time one is added.\npub const VARS: &[&str] = &[\n \"DASHSCOPE_API_KEY\",\n \"OPENROUTER_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"OPENAI_API_KEY\",\n \"GEMINI_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n];\n\n/// The provider a variable authenticates, spelled as `CREW_PROVIDER` and\n/// `pick_provider` spell it.\npub fn provider_for(var: &str) -> Option<&'static str> {\n // From the auth registry, so adding a provider row teaches the\n // credential store about it too — no second name table here.\n crate::broker::auth::registry::name_for_var(var)\n}\n\n/// The on-disk shape. `provider` is the pin written when a key is saved: with\n/// `pick_provider`'s fixed DashScope → OpenRouter → Anthropic order, supplying\n/// an Anthropic key while a DashScope key exists would otherwise change\n/// nothing the user can see.\n#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub struct Store {\n #[serde(default)]\n pub provider: Option<String>,\n #[serde(default)]\n pub keys: BTreeMap<String, String>,\n}\n\n/// Hand-written so the key VALUES can never be printed. A derived `Debug`\n/// would put raw secrets into any `dbg!`, `assert_eq!` failure, `anyhow`\n/// context or panic message that ever touches a `Store` — none of which the\n/// author of that line would be thinking about. The names and the provider pin\n/// are not secret and stay visible, so the output is still useful.\nimpl std::fmt::Debug for Store {\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n let redacted: BTreeMap<&str, &str> = self\n .keys\n .keys()\n .map(|k| (k.as_str(), \"<redacted>\"))\n .collect();\n f.debug_struct(\"Store\")\n .field(\"provider\", &self.provider)\n .field(\"keys\", &redacted)\n .finish()\n }\n}\n\n/// `<config_dir>/crew/credentials.json`, a sibling of `config.toml` — unless\n/// `CREW_CREDENTIALS_PATH` is set (non-empty), in which case that path wins.\n///\n/// This exists purely for test isolation. The store is a process-global,\n/// real-disk singleton with no equivalent of `CREW_PROJECT_DIR`'s CWD seam:\n/// every other on-disk store here (`specialists`, `plugins`, `sessionlog`)\n/// takes a base directory tests can point elsewhere, but credentials always\n/// resolved through `dirs::config_dir()` with no override. That made\n/// `broker::testenv::no_provider()` — which promises to force provider\n/// discovery to fail even on a machine with real keys exported — silently\n/// unable to keep that promise once a real `credentials.json` existed: reads\n/// through `forced_provider()`/`shellenv::hydrate()` reached past the guard\n/// straight to the real config directory. Do not remove this override to\n/// \"simplify\" the function; it is what lets tests neutralise the store the\n/// same way they neutralise the environment.\npub fn path() -> Option<PathBuf> {\n if let Ok(p) = std::env::var(\"CREW_CREDENTIALS_PATH\") {\n if !p.is_empty() {\n return Some(PathBuf::from(p));\n }\n }\n dirs::config_dir().map(|d| d.join(\"crew\").join(\"credentials.json\"))\n}\n\n/// The stored credentials, or an empty store. EVERY failure — no config dir,\n/// no file, unreadable, malformed JSON — reads as empty: a broken credentials\n/// file must never stop crew from starting.\npub fn load() -> Store {\n path().map(|p| load_from(&p)).unwrap_or_default()\n}\n\n/// [`load`] from an explicit path (the testable half).\npub fn load_from(path: &Path) -> Store {\n std::fs::read_to_string(path)\n .ok()\n .and_then(|t| serde_json::from_str(&t).ok())\n .unwrap_or_default()\n}\n\n/// Store `value` for `var`, optionally moving the provider pin. An empty\n/// `value` REMOVES the key rather than storing a blank — an empty\n/// `ANTHROPIC_API_KEY` still outranks a valid OAuth profile, so a blank is\n/// worse than nothing.\n///\n/// Never logs `value`.\npub fn save_key(var: &str, value: &str, provider: Option<&str>) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_key_at(&path, var, value, provider)\n}\n\n/// Move the provider pin without touching any key.\n///\n/// Picking a model whose vendor crew can reach with a key the user already\n/// holds should just work — `pick_provider`'s fixed order would otherwise\n/// keep answering with whichever provider happens to rank first, and the user\n/// would have to know that `CREW_PROVIDER` exists to get the model they just\n/// chose. Storing the pin is how the choice survives to the next request.\npub fn save_pin(provider: &str) -> anyhow::Result<()> {\n let path =\n path().ok_or_else(|| anyhow::anyhow!(\"no config directory to store credentials in\"))?;\n save_pin_at(&path, provider)\n}\n\n/// [`save_pin`] at an explicit path (the testable half).\npub fn save_pin_at(path: &Path, provider: &str) -> anyhow::Result<()> {\n let mut store = load_from(path);\n store.provider = Some(provider.to_string());\n let bytes = serde_json::to_vec_pretty(&store)?;\n if let Some(dir) = path.parent() {\n std::fs::create_dir_all(dir)?;\n }\n write_atomic(path, &bytes)\n}\n\n/// [`save_key`] at an explicit path (the testable half).\npub fn save_key_at(\n path: &Path,\n var: &str,\n value: &str,\n provider: Option<&str>,\n) -> anyhow::Result<()> {\n if !VARS.contains(&var) {\n anyhow::bail!(\"{var} is not a provider key crew stores\");\n }\n let dir = path\n .parent()\n .ok_or_else(|| anyhow::anyhow!(\"credentials path has no parent directory\"))?;\n std::fs::create_dir_all(dir)?;\n #[cfg(unix)]\n {\n use std::os::unix::fs::PermissionsExt;\n std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;\n }\n let mut store = load_from(path);\n if value.is_empty() {\n store.keys.remove(var);\n } else {\n store.keys.insert(var.to_string(), value.to_string());\n }\n if let Some(p) = provider {\n store.provider = Some(p.to_string());\n }\n write_atomic(path, &serde_json::to_vec_pretty(&store)?)\n}\n\n/// Write via a same-directory temp file created 0600 BEFORE any content lands\n/// in it, then rename over the target. There is never a moment when the\n/// secret exists in a world-readable file, and a crash leaves either the old\n/// file or the temp — never a truncated one.\n///\n/// Public because the credential store is not the only file that must never be\n/// world-readable: `crew-app`'s `history` captures every line typed into the\n/// input bar, which can include a secret typed into the wrong surface, and it\n/// gets the same treatment through here rather than a second copy of this\n/// reasoning. `std::fs::write` is the thing to avoid — it creates 0644 and can\n/// only be chmod'd *after* the bytes have landed.\npub fn write_atomic(path: &Path, bytes: &[u8]) -> anyhow::Result<()> {\n use std::io::Write;\n // `<name>.tmp`, appended rather than substituted: `with_extension` would\n // have to know the target's extension (and this now writes an extensionless\n // history file too). For `credentials.json` both spell `credentials.json.tmp`.\n let mut tmp = path.as_os_str().to_owned();\n tmp.push(\".tmp\");\n let tmp = PathBuf::from(tmp);\n // Remove any stale temp entry before creating our own. `create_new` alone\n // would fail forever after an interrupted write; removing first and then\n // refusing to open anything that still exists means we only ever write to\n // an inode we just made, at the mode we chose. `remove_file` unlinks a\n // symlink itself rather than following it, so a planted link is destroyed,\n // not written through.\n let _ = std::fs::remove_file(&tmp);\n let mut opts = std::fs::OpenOptions::new();\n opts.write(true).create_new(true);\n #[cfg(unix)]\n {\n use std::os::unix::fs::OpenOptionsExt;\n opts.mode(0o600);\n }\n let mut f = opts.open(&tmp)?;\n f.write_all(bytes)?;\n f.sync_all()?;\n drop(f);\n std::fs::rename(&tmp, path)?;\n Ok(())\n}\n\n#[cfg(test)]\n#[path = \"credentials_tests.rs\"]\nmod tests;\n"}}}}