mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
Move the credentials module from tui into codewhale-runtime
First placement-independent step of the S7=(c) decision (config, credentials and OAuth move toward codewhale-runtime). credentials has no dependency on any other tui module (0 outbound edges) and is the only module the config cluster was blocked on at module level (23 references), so it moves first and alone. - scripts/split/move-modules.py credentials rewrote the seven callers (config, config/credential_resolve, config/tests, provider_readiness, runtime_api/secrets, tui/provider_picker, client test cases) to codewhale_runtime::credentials. No shim or re-export stays in tui. - scripts/split/widen.py widened exactly the 38 items the compiler named (pub(crate) to pub) and deleted three expect(dead_code) attributes the export fulfils. Token and key material is unchanged: Credential keeps its hand-written Debug that prints only the variant tag, and secrets stay behind the existing store trait. - MapAuthContext is gated on any(test, feature = "test-support") instead of cfg(test) so tui tests can still use it across the crate boundary; the tui dev-dependency on runtime already enables test-support. - codewhale-runtime gains a codewhale-secrets dependency (Cargo.toml and Cargo.lock). codewhale-secrets is a leaf; the command-crate-boundaries check still passes with runtime free of UI packages. - One module comment that named crate::config::credential_resolve now names codewhale_tui::config::credential_resolve. THIRD_PARTY_NOTICES and the old decomposition roadmap carry the path sweep; the roadmap still plans a later move into codewhale-secrets, which S7=(c) supersedes. - dead-code budget lowered 212 to 209. Size: codewhale-runtime production lines 14,699 to 15,416 (closure_report), tui 965,801 to 965,081. This is NOT progress on the 79-module SCC (credentials was never in it) and the crate split is not done. Honest headroom note for the founder: moving the rest of the S7=(c) cluster (config 14,105, settings 5,935, config_persistence 2,180, oauth 9,896, codex_model_cache 608 = 32,724 production lines) would take runtime to about 48,140, within about 1,900 of the 50k ceiling and far from the 10k typical target, with no room for the 13 other wave-0 modules (5,988 lines). Verified (macOS, local only; no hosted CI, Linux/FreeBSD/Windows compile): - scripts/dev-cargo.sh build -p codewhale-cli --bin codewhale --locked: exit 0 (known ld __eh_frame warning only). - nextest -p codewhale-runtime -p codewhale-tui --lib, filters runtime credentials:: and tui credential / secrets / provider_readiness / provider_picker / config::tests: 774 run, 774 passed, 0 failed, 14792 skipped (10 runtime credentials tests, 755 tui tests). - module_graph --check 121 closure modules PASS, blocking calls 543/165, command-crate-boundaries PASS, dead-code 209 (budget updated). Not verified: a signing-in or key-saving run through the product. The exercised paths are the existing unit and API tests only. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
b4103aa7bb
commit
0c2ab7d577
19 files changed
+89
-90
No files matched your search
Generated
+1
@@ -1202,6 +1202,7 @@ dependencies = [
|
||||
"codewhale-models",
|
||||
"codewhale-palette",
|
||||
"codewhale-paths",
|
||||
"codewhale-secrets",
|
||||
"colored",
|
||||
"fd-lock",
|
||||
"libc",
|
||||
|
||||
@@ -45,6 +45,7 @@ codewhale-lane = { path = "../lane", version = "0.10.2" }
|
||||
codewhale-memory = { path = "../memory", version = "0.10.2" }
|
||||
codewhale-models = { path = "../models", version = "0.10.2" }
|
||||
codewhale-palette = { path = "../palette", version = "0.10.2", default-features = false }
|
||||
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
|
||||
colored = "3.0.0"
|
||||
fd-lock = "4.0.4"
|
||||
lru = "0.18"
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
//! stats the filesystem (#5772): external credential discovery is
|
||||
//! metadata-only, so the trait deliberately has no file probe.
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Ambient environment access for credential resolution.
|
||||
pub(crate) trait AuthContext: Send + Sync {
|
||||
pub trait AuthContext: Send + Sync {
|
||||
/// Read an environment variable, treating blank values as unset — a blank
|
||||
/// `DEEPSEEK_API_KEY=` is a leftover export, not a credential.
|
||||
fn env(&self, name: &str) -> Option<String>;
|
||||
@@ -23,7 +23,7 @@ pub(crate) trait AuthContext: Send + Sync {
|
||||
|
||||
/// The real process environment.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub(crate) struct ProcessAuthContext;
|
||||
pub struct ProcessAuthContext;
|
||||
|
||||
impl AuthContext for ProcessAuthContext {
|
||||
fn env(&self, name: &str) -> Option<String> {
|
||||
@@ -34,25 +34,25 @@ impl AuthContext for ProcessAuthContext {
|
||||
}
|
||||
|
||||
/// Test double: a fixed set of variables.
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct MapAuthContext {
|
||||
pub struct MapAuthContext {
|
||||
env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl MapAuthContext {
|
||||
pub(crate) fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn with_env(mut self, name: &str, value: &str) -> Self {
|
||||
pub fn with_env(mut self, name: &str, value: &str) -> Self {
|
||||
self.env.insert(name.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl AuthContext for MapAuthContext {
|
||||
fn env(&self, name: &str) -> Option<String> {
|
||||
self.env
|
||||
@@ -11,7 +11,7 @@
|
||||
//! | `packages/ai/src/auth/types.ts` | [`Credential`], [`CredentialStore`], [`AuthContext`] |
|
||||
//! | `packages/ai/src/auth/credential-store.ts`| [`store::InMemoryCredentialStore`] |
|
||||
//! | `packages/ai/src/auth/context.ts` | [`context::ProcessAuthContext`] |
|
||||
//! | `packages/ai/src/auth/resolve.ts` | `crate::config::credential_resolve` |
|
||||
//! | `packages/ai/src/auth/resolve.ts` | `codewhale_tui::config::credential_resolve` |
|
||||
//!
|
||||
//! This is a **design port into idiomatic Rust, not a line-for-line copy**.
|
||||
//! pi's module is async TypeScript over a `Provider` record with a single
|
||||
@@ -55,16 +55,16 @@
|
||||
//! variant tag, so a credential cannot reach a log line through `{:?}` on a
|
||||
//! surrounding struct.
|
||||
|
||||
pub(crate) mod context;
|
||||
pub mod context;
|
||||
pub(crate) mod source;
|
||||
pub(crate) mod store;
|
||||
pub mod store;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub(crate) use context::AuthContext;
|
||||
pub(crate) use source::{CredentialProbe, CredentialResolution, CredentialSource};
|
||||
pub(crate) use store::{CredentialInfo, CredentialStore};
|
||||
pub use context::AuthContext;
|
||||
pub use source::{CredentialProbe, CredentialResolution, CredentialSource};
|
||||
pub use store::{CredentialInfo, CredentialStore};
|
||||
|
||||
/// One type-tagged credential per provider — pi's `Credential` union.
|
||||
///
|
||||
@@ -73,7 +73,7 @@ pub(crate) use store::{CredentialInfo, CredentialStore};
|
||||
/// unify; the OAuth variant carries only what a status surface needs, so
|
||||
/// adopting this type never moves a token between stores.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub(crate) enum Credential {
|
||||
pub enum Credential {
|
||||
/// A bearer API key held in Codewhale's durable store or process memory.
|
||||
ApiKey { key: String },
|
||||
/// An OAuth access token plus its expiry, if the flow reported one.
|
||||
@@ -84,7 +84,6 @@ pub(crate) enum Credential {
|
||||
/// production path mints one yet. The variant is kept because it is half
|
||||
/// of the ported contract and the store's serialization guarantee exists
|
||||
/// precisely for it.
|
||||
#[cfg_attr(not(test), expect(dead_code))]
|
||||
OAuth {
|
||||
access: String,
|
||||
expires_at_unix_secs: Option<i64>,
|
||||
@@ -94,7 +93,7 @@ pub(crate) enum Credential {
|
||||
impl Credential {
|
||||
/// Only [`store::InMemoryCredentialStore::list`] needs this today; the
|
||||
/// secret-store adapter knows every slot it holds is an api key.
|
||||
pub(crate) fn kind(&self) -> CredentialKind {
|
||||
pub fn kind(&self) -> CredentialKind {
|
||||
match self {
|
||||
Self::ApiKey { .. } => CredentialKind::ApiKey,
|
||||
Self::OAuth { .. } => CredentialKind::OAuth,
|
||||
@@ -103,7 +102,7 @@ impl Credential {
|
||||
|
||||
/// Borrow the secret. Callers must not log or render the result; this is
|
||||
/// the single narrow accessor so `rg "expose_secret"` finds every use.
|
||||
pub(crate) fn expose_secret(&self) -> &str {
|
||||
pub fn expose_secret(&self) -> &str {
|
||||
match self {
|
||||
Self::ApiKey { key } => key,
|
||||
Self::OAuth { access, .. } => access,
|
||||
@@ -130,7 +129,7 @@ impl std::fmt::Debug for Credential {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CredentialKind {
|
||||
pub enum CredentialKind {
|
||||
ApiKey,
|
||||
/// See the note on [`Credential::OAuth`]: no production store mints one
|
||||
/// yet.
|
||||
@@ -17,16 +17,16 @@ use std::borrow::Cow;
|
||||
/// One place credential resolution looked, and what would put a credential
|
||||
/// there. Labels only; never a value.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CredentialProbe {
|
||||
pub struct CredentialProbe {
|
||||
/// Where we looked, e.g. `"env DEEPSEEK_API_KEY"` or `"secret store slot \"deepseek\""`.
|
||||
pub(crate) place: Cow<'static, str>,
|
||||
/// What the user would do to make this place answer, if there is a
|
||||
/// one-line answer. `None` when the place is informational only.
|
||||
pub(crate) fix: Option<Cow<'static, str>>,
|
||||
pub fix: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl CredentialProbe {
|
||||
pub(crate) fn with_fix(
|
||||
pub fn with_fix(
|
||||
place: impl Into<Cow<'static, str>>,
|
||||
fix: impl Into<Cow<'static, str>>,
|
||||
) -> Self {
|
||||
@@ -40,7 +40,7 @@ impl CredentialProbe {
|
||||
/// The place a credential was resolved from, or the fact that no place had
|
||||
/// one. This is the single value every readiness surface should render.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum CredentialSource {
|
||||
pub enum CredentialSource {
|
||||
/// `auth_mode = "none"`: the route intentionally sends no credential.
|
||||
AuthModeNone,
|
||||
/// A keyless self-hosted or loopback route.
|
||||
@@ -92,13 +92,13 @@ pub(crate) enum CredentialSource {
|
||||
}
|
||||
|
||||
impl CredentialSource {
|
||||
pub(crate) fn is_present(&self) -> bool {
|
||||
pub fn is_present(&self) -> bool {
|
||||
!matches!(self, Self::Missing { .. })
|
||||
}
|
||||
|
||||
/// Short human-readable label, in pi's spirit: the name of the place, not
|
||||
/// a sentence. Safe to render anywhere — never contains a secret.
|
||||
pub(crate) fn label(&self) -> Cow<'static, str> {
|
||||
pub fn label(&self) -> Cow<'static, str> {
|
||||
match self {
|
||||
Self::AuthModeNone => Cow::Borrowed("auth_mode = \"none\""),
|
||||
Self::KeylessRoute { base_url } => Cow::Owned(format!(
|
||||
@@ -122,7 +122,7 @@ impl CredentialSource {
|
||||
}
|
||||
|
||||
/// The ordered places that were probed, for a failed resolution.
|
||||
pub(crate) fn probed(&self) -> &[CredentialProbe] {
|
||||
pub fn probed(&self) -> &[CredentialProbe] {
|
||||
match self {
|
||||
Self::Missing { probed } => probed,
|
||||
_ => &[],
|
||||
@@ -134,29 +134,29 @@ impl CredentialSource {
|
||||
/// readiness surfaces need the source, and the request path already has its
|
||||
/// own resolver that returns the secret.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CredentialResolution {
|
||||
pub(crate) source: CredentialSource,
|
||||
pub struct CredentialResolution {
|
||||
pub source: CredentialSource,
|
||||
}
|
||||
|
||||
impl CredentialResolution {
|
||||
pub(crate) fn found(source: CredentialSource) -> Self {
|
||||
pub fn found(source: CredentialSource) -> Self {
|
||||
debug_assert!(source.is_present());
|
||||
Self { source }
|
||||
}
|
||||
|
||||
pub(crate) fn missing(probed: Vec<CredentialProbe>) -> Self {
|
||||
pub fn missing(probed: Vec<CredentialProbe>) -> Self {
|
||||
Self {
|
||||
source: CredentialSource::Missing { probed },
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_present(&self) -> bool {
|
||||
pub fn is_present(&self) -> bool {
|
||||
self.source.is_present()
|
||||
}
|
||||
|
||||
/// One line naming the places checked, for a status row. Empty when the
|
||||
/// resolution succeeded.
|
||||
pub(crate) fn checked_places(&self) -> String {
|
||||
pub fn checked_places(&self) -> String {
|
||||
self.source
|
||||
.probed()
|
||||
.iter()
|
||||
@@ -166,7 +166,7 @@ impl CredentialResolution {
|
||||
}
|
||||
|
||||
/// The first actionable fix among the probed places, if any.
|
||||
pub(crate) fn first_fix(&self) -> Option<&str> {
|
||||
pub fn first_fix(&self) -> Option<&str> {
|
||||
self.source
|
||||
.probed()
|
||||
.iter()
|
||||
@@ -28,8 +28,8 @@ use super::{Credential, CredentialKind};
|
||||
|
||||
/// Non-secret credential metadata for account/status enumeration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CredentialInfo {
|
||||
pub(crate) provider_id: String,
|
||||
pub struct CredentialInfo {
|
||||
pub provider_id: String,
|
||||
pub(crate) kind: CredentialKind,
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ pub(crate) struct CredentialInfo {
|
||||
/// credential from enumeration (status, `/provider`, logout). `list` fails
|
||||
/// only when enumeration itself cannot run. `list` must not execute
|
||||
/// configured API-key commands or open network flows.
|
||||
pub(crate) trait CredentialStore: Send + Sync {
|
||||
pub trait CredentialStore: Send + Sync {
|
||||
/// Read the stored credential, possibly expired. Display/status use.
|
||||
fn read(&self, provider_id: &str) -> Result<Option<Credential>>;
|
||||
|
||||
@@ -68,7 +68,6 @@ pub(crate) trait CredentialStore: Send + Sync {
|
||||
/// sequence instead. Collapsing them onto `modify` would change their
|
||||
/// error messages and rollback shape, which is deliberately out of this
|
||||
/// change's scope.
|
||||
#[cfg_attr(not(test), expect(dead_code))]
|
||||
fn modify(
|
||||
&self,
|
||||
provider_id: &str,
|
||||
@@ -82,7 +81,6 @@ pub(crate) trait CredentialStore: Send + Sync {
|
||||
/// than through this trait. Routing it here is the remaining half of the
|
||||
/// port and is deliberately not folded into this change — logout is
|
||||
/// security-sensitive and deserves its own commit and its own tests.
|
||||
#[cfg_attr(not(test), expect(dead_code))]
|
||||
fn delete(&self, provider_id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -106,7 +104,7 @@ fn provider_lock(provider_id: &str) -> Arc<Mutex<()>> {
|
||||
}
|
||||
|
||||
/// Run `body` holding this provider's write lock.
|
||||
pub(crate) fn with_provider_write_lock<T>(provider_id: &str, body: impl FnOnce() -> T) -> T {
|
||||
pub fn with_provider_write_lock<T>(provider_id: &str, body: impl FnOnce() -> T) -> T {
|
||||
let lock = provider_lock(provider_id);
|
||||
let _guard = lock
|
||||
.lock()
|
||||
@@ -120,7 +118,7 @@ pub(crate) fn with_provider_write_lock<T>(provider_id: &str, body: impl FnOnce()
|
||||
/// covers every slot cannot deadlock with a per-provider save (which holds
|
||||
/// only one). Callers that also take the xAI OAuth lifecycle lock must take
|
||||
/// that lock first, matching the documented xAI-then-config order.
|
||||
pub(crate) fn with_provider_write_locks<T>(
|
||||
pub fn with_provider_write_locks<T>(
|
||||
provider_ids: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
body: impl FnOnce() -> T,
|
||||
) -> T {
|
||||
@@ -220,7 +218,7 @@ impl CredentialStore for InMemoryCredentialStore {
|
||||
/// `codewhale_secrets::Secrets` already owns. Its value is that every write now
|
||||
/// goes through `modify` under the provider's lock, so a save racing a rotate
|
||||
/// no longer interleaves.
|
||||
pub(crate) struct SecretStoreCredentials {
|
||||
pub struct SecretStoreCredentials {
|
||||
secrets: codewhale_secrets::Secrets,
|
||||
/// Slots to probe for `list`. The backing keyring exposes no key
|
||||
/// enumeration, so the caller supplies the known slot names.
|
||||
@@ -228,7 +226,7 @@ pub(crate) struct SecretStoreCredentials {
|
||||
}
|
||||
|
||||
impl SecretStoreCredentials {
|
||||
pub(crate) fn new(secrets: codewhale_secrets::Secrets, known_slots: Vec<String>) -> Self {
|
||||
pub fn new(secrets: codewhale_secrets::Secrets, known_slots: Vec<String>) -> Self {
|
||||
Self {
|
||||
secrets,
|
||||
known_slots,
|
||||
File renamed without changes.
@@ -17,6 +17,7 @@ pub mod browser;
|
||||
pub mod child_env;
|
||||
pub mod context_budget;
|
||||
pub mod continual_harness;
|
||||
pub mod credentials;
|
||||
pub mod dsh_credentials;
|
||||
pub mod external_credentials;
|
||||
pub mod fast_hash;
|
||||
|
||||
@@ -1102,7 +1102,7 @@
|
||||
&(config).test_identity_for_kind(ProviderKind::Xai)
|
||||
)
|
||||
.source,
|
||||
crate::credentials::CredentialSource::OAuth {
|
||||
codewhale_runtime::credentials::CredentialSource::OAuth {
|
||||
flow: "xAI".to_string(),
|
||||
account: Some("[email protected]".to_string()),
|
||||
}
|
||||
|
||||
+20
-20
@@ -15,7 +15,7 @@ use serde_json::json;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use crate::credentials::CredentialStore;
|
||||
use codewhale_runtime::credentials::CredentialStore;
|
||||
use crate::hooks::HooksConfig;
|
||||
use codewhale_runtime::audit::log_sensitive_event;
|
||||
use codewhale_runtime::features::{Feature, Features, FeaturesToml, is_known_feature_key};
|
||||
@@ -1926,7 +1926,7 @@ impl TranscriptConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AccountModelAccess {
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) credential: crate::credentials::Credential,
|
||||
pub(crate) credential: codewhale_runtime::credentials::Credential,
|
||||
pub(crate) expires_at: i64,
|
||||
pub(crate) profile: Option<String>,
|
||||
}
|
||||
@@ -1947,7 +1947,7 @@ pub struct Config {
|
||||
pub(crate) session_provider_keys_only: bool,
|
||||
#[serde(skip)]
|
||||
pub(crate) session_provider_keys: std::sync::Arc<
|
||||
parking_lot::RwLock<HashMap<ProviderIdentity, (String, crate::credentials::Credential)>>,
|
||||
parking_lot::RwLock<HashMap<ProviderIdentity, (String, codewhale_runtime::credentials::Credential)>>,
|
||||
>,
|
||||
#[serde(skip)]
|
||||
pub(crate) required_account_model_owner: Option<String>,
|
||||
@@ -5900,8 +5900,8 @@ impl Config {
|
||||
pub(crate) fn session_policy_without_key(
|
||||
&self,
|
||||
identity: &ProviderIdentity,
|
||||
) -> Option<crate::credentials::CredentialSource> {
|
||||
use crate::credentials::{CredentialProbe, CredentialSource};
|
||||
) -> Option<codewhale_runtime::credentials::CredentialSource> {
|
||||
use codewhale_runtime::credentials::{CredentialProbe, CredentialSource};
|
||||
if !self.session_provider_keys_only {
|
||||
return None;
|
||||
}
|
||||
@@ -5991,8 +5991,8 @@ impl Config {
|
||||
}
|
||||
if let Some(source) = scoped.session_policy_without_key(identity) {
|
||||
return match source {
|
||||
crate::credentials::CredentialSource::AuthModeNone
|
||||
| crate::credentials::CredentialSource::KeylessRoute { .. } => Some(generation("")),
|
||||
codewhale_runtime::credentials::CredentialSource::AuthModeNone
|
||||
| codewhale_runtime::credentials::CredentialSource::KeylessRoute { .. } => Some(generation("")),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
@@ -6189,15 +6189,15 @@ impl Config {
|
||||
if let Some(key) = self.session_provider_api_key(&identity) {
|
||||
return Ok((
|
||||
key,
|
||||
crate::credentials::CredentialSource::SessionKey
|
||||
codewhale_runtime::credentials::CredentialSource::SessionKey
|
||||
.label()
|
||||
.into_owned(),
|
||||
));
|
||||
}
|
||||
if let Some(source) = self.session_policy_without_key(&identity) {
|
||||
return match &source {
|
||||
crate::credentials::CredentialSource::AuthModeNone
|
||||
| crate::credentials::CredentialSource::KeylessRoute { .. } => {
|
||||
codewhale_runtime::credentials::CredentialSource::AuthModeNone
|
||||
| codewhale_runtime::credentials::CredentialSource::KeylessRoute { .. } => {
|
||||
Ok((String::new(), source.label().into_owned()))
|
||||
}
|
||||
_ => anyhow::bail!(
|
||||
@@ -11020,7 +11020,7 @@ fn save_root_api_key_for_secret_slot(
|
||||
if let Some(secrets) = credential_secret_store() {
|
||||
// Same read-modify-write as the per-provider save below; hold the slot's
|
||||
// write lock across snapshot, store write, config write, and rollback.
|
||||
return crate::credentials::store::with_provider_write_lock(secret_slot, || {
|
||||
return codewhale_runtime::credentials::store::with_provider_write_lock(secret_slot, || {
|
||||
let prior_secret = secrets.get(secret_slot);
|
||||
match prior_secret.as_ref() {
|
||||
Ok(prior) => match secrets.set(secret_slot, trimmed) {
|
||||
@@ -11680,8 +11680,8 @@ fn save_api_key_for_identity_unlocked(
|
||||
// so a concurrent save or logout on the same slot cannot interleave and
|
||||
// leave the secret store and the config document disagreeing. This is
|
||||
// the `modify`-is-the-only-write-path rule ported from pi-mono; see
|
||||
// `crate::credentials::store`.
|
||||
return crate::credentials::store::with_provider_write_lock(secret_slot, || {
|
||||
// `codewhale_runtime::credentials::store`.
|
||||
return codewhale_runtime::credentials::store::with_provider_write_lock(secret_slot, || {
|
||||
let prior_secret = secrets.get(secret_slot);
|
||||
match prior_secret.as_ref() {
|
||||
Ok(prior) => match secrets.set(secret_slot, api_key) {
|
||||
@@ -12201,11 +12201,11 @@ fn provider_secret_store_api_key_with_mode(
|
||||
codewhale_secrets::Secrets::auto_detect()
|
||||
};
|
||||
// Read through the credential-store trait so every read of a durable slot
|
||||
// goes through one adapter (`crate::credentials::store`), and the value is
|
||||
// goes through one adapter (`codewhale_runtime::credentials::store`), and the value is
|
||||
// carried as a type-tagged `Credential` rather than a bare String that can
|
||||
// drift into a log line.
|
||||
let store =
|
||||
crate::credentials::store::SecretStoreCredentials::new(secrets, known_secret_store_slots());
|
||||
codewhale_runtime::credentials::store::SecretStoreCredentials::new(secrets, known_secret_store_slots());
|
||||
let primary = store
|
||||
.read(provider_secret_store_slot(provider))
|
||||
.ok()
|
||||
@@ -12233,7 +12233,7 @@ fn provider_secret_store_api_key_with_mode(
|
||||
/// Every durable credential slot CodeWhale knows how to write.
|
||||
///
|
||||
/// The backing keyring exposes no key enumeration, so
|
||||
/// [`crate::credentials::store::SecretStoreCredentials::list`] is given the
|
||||
/// [`codewhale_runtime::credentials::store::SecretStoreCredentials::list`] is given the
|
||||
/// slot names to probe. Deduplicated because shared-account families collapse
|
||||
/// several providers onto one slot.
|
||||
fn known_secret_store_slots() -> Vec<String> {
|
||||
@@ -12398,7 +12398,7 @@ fn clear_api_key_unlocked() -> Result<()> {
|
||||
// Same read-modify-write as the saves: hold every durable slot's write
|
||||
// lock across the config-document mutation and the store deletes so a
|
||||
// concurrent save cannot interleave and leave the two disagreeing.
|
||||
crate::credentials::store::with_provider_write_locks(
|
||||
codewhale_runtime::credentials::store::with_provider_write_locks(
|
||||
known_secret_store_slots(),
|
||||
clear_api_key_under_slot_locks,
|
||||
)
|
||||
@@ -12471,14 +12471,14 @@ fn clear_all_provider_api_keys_from_secret_store(
|
||||
secrets: codewhale_secrets::Secrets,
|
||||
) -> Vec<String> {
|
||||
let mut failures = Vec::new();
|
||||
let store = crate::credentials::store::SecretStoreCredentials::new(
|
||||
let store = codewhale_runtime::credentials::store::SecretStoreCredentials::new(
|
||||
secrets.clone(),
|
||||
known_secret_store_slots(),
|
||||
);
|
||||
// `list` enumerates the slots that actually hold something, without
|
||||
// exposing any value — the deduplication that used to live here is now the
|
||||
// slot table's job.
|
||||
let stored: Vec<crate::credentials::CredentialInfo> = match store.list() {
|
||||
let stored: Vec<codewhale_runtime::credentials::CredentialInfo> = match store.list() {
|
||||
Ok(stored) => stored,
|
||||
Err(error) => {
|
||||
failures.push(format!("secret store enumeration: {error}"));
|
||||
@@ -12513,7 +12513,7 @@ pub fn clear_active_provider_api_key(provider: &str) -> Result<()> {
|
||||
fn clear_active_provider_api_key_unlocked(provider: &str) -> Result<()> {
|
||||
let slot = compatibility_for_id(provider).map(|row| provider_secret_store_slot(row.kind));
|
||||
match slot {
|
||||
Some(slot) => crate::credentials::store::with_provider_write_lock(slot, || {
|
||||
Some(slot) => codewhale_runtime::credentials::store::with_provider_write_lock(slot, || {
|
||||
clear_active_provider_api_key_under_lock(provider)
|
||||
}),
|
||||
None => clear_active_provider_api_key_under_lock(provider),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The one place provider credential precedence is decided.
|
||||
//!
|
||||
//! Ported from pi-mono `packages/ai/src/auth/resolve.ts` (MIT, Copyright (c)
|
||||
//! 2025 Mario Zechner; full notice in `crate::credentials`). The idea taken is
|
||||
//! 2025 Mario Zechner; full notice in `codewhale_runtime::credentials`). The idea taken is
|
||||
//! pi's: a single resolver, one precedence rule stated in a doc comment beside
|
||||
//! it, and a result that names the place it resolved from. The walk itself is
|
||||
//! CodeWhale's — it is the former body of `has_api_key_for`, moved here
|
||||
@@ -64,7 +64,7 @@
|
||||
//! existing helper and discards the value with `.is_some()`.
|
||||
|
||||
use super::*;
|
||||
use crate::credentials::{
|
||||
use codewhale_runtime::credentials::{
|
||||
AuthContext, CredentialProbe, CredentialResolution, CredentialSource,
|
||||
context::ProcessAuthContext,
|
||||
};
|
||||
@@ -398,7 +398,7 @@ fn resolve_external_grant(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::credentials::context::MapAuthContext;
|
||||
use codewhale_runtime::credentials::context::MapAuthContext;
|
||||
use crate::test_support::{EnvVarGuard, lock_test_env};
|
||||
|
||||
fn deepseek_config() -> Config {
|
||||
|
||||
@@ -4655,7 +4655,7 @@ fn single_provider_logout_holds_the_slot_write_lock_across_config_and_store() ->
|
||||
let (held_tx, held_rx) = mpsc::channel();
|
||||
let (release_tx, release_rx) = mpsc::channel();
|
||||
let holder = std::thread::spawn(move || {
|
||||
crate::credentials::store::with_provider_write_lock("openrouter", || {
|
||||
codewhale_runtime::credentials::store::with_provider_write_lock("openrouter", || {
|
||||
let _ = held_tx.send(());
|
||||
let _ = release_rx.recv();
|
||||
});
|
||||
@@ -4731,7 +4731,7 @@ fn full_logout_holds_every_slot_write_lock_across_config_and_store() -> Result<(
|
||||
let (held_tx, held_rx) = mpsc::channel();
|
||||
let (release_tx, release_rx) = mpsc::channel();
|
||||
let holder = std::thread::spawn(move || {
|
||||
crate::credentials::store::with_provider_write_lock("openrouter", || {
|
||||
codewhale_runtime::credentials::store::with_provider_write_lock("openrouter", || {
|
||||
let _ = held_tx.send(());
|
||||
let _ = release_rx.recv();
|
||||
});
|
||||
@@ -15646,7 +15646,7 @@ fn remembered_model_pick_defers_to_the_configured_spelling() {
|
||||
/// slot was skipped, and why.
|
||||
#[test]
|
||||
fn picker_and_request_path_disagree_when_the_secret_slot_marker_is_missing() -> Result<()> {
|
||||
use crate::credentials::CredentialSource;
|
||||
use codewhale_runtime::credentials::CredentialSource;
|
||||
|
||||
let _lock = lock_test_env();
|
||||
let temp_root = tempfile::tempdir()?;
|
||||
|
||||
@@ -37,7 +37,6 @@ mod conformance;
|
||||
mod context_report;
|
||||
mod core;
|
||||
mod cost_status;
|
||||
mod credentials;
|
||||
/// Guarded installation I/O reusing Engine's confined file primitives.
|
||||
pub mod delivery_files;
|
||||
mod dependencies;
|
||||
|
||||
@@ -151,8 +151,8 @@ pub(crate) fn credential_state_for_provider(
|
||||
}
|
||||
if let Some(source) = config.session_policy_without_key(identity) {
|
||||
return match source {
|
||||
crate::credentials::CredentialSource::AuthModeNone => CredentialState::NoAuth,
|
||||
crate::credentials::CredentialSource::KeylessRoute { .. } => CredentialState::Local,
|
||||
codewhale_runtime::credentials::CredentialSource::AuthModeNone => CredentialState::NoAuth,
|
||||
codewhale_runtime::credentials::CredentialSource::KeylessRoute { .. } => CredentialState::Local,
|
||||
_ => CredentialState::MissingKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ fn install_account_model_access(
|
||||
}
|
||||
*access = Some(crate::config::AccountModelAccess {
|
||||
session_id: request.session_id,
|
||||
credential: crate::credentials::Credential::ApiKey { key: request.key },
|
||||
credential: codewhale_runtime::credentials::Credential::ApiKey { key: request.key },
|
||||
expires_at: request.expires_at,
|
||||
profile: profile.map(str::to_string),
|
||||
});
|
||||
@@ -363,7 +363,7 @@ pub(super) async fn set_provider_key(
|
||||
)?;
|
||||
let target = credential_store.as_mut().unwrap_or(&mut store);
|
||||
let slot = codewhale_config::credentials::provider_slot(kind_owned);
|
||||
crate::credentials::store::with_provider_write_lock(slot, || {
|
||||
codewhale_runtime::credentials::store::with_provider_write_lock(slot, || {
|
||||
codewhale_config::credentials::set_provider_api_key(
|
||||
target, &secrets, kind_owned, &key_owned,
|
||||
)
|
||||
@@ -394,7 +394,7 @@ pub(super) async fn set_provider_key(
|
||||
}
|
||||
config.session_provider_keys.write().insert(
|
||||
provider_owned.clone(),
|
||||
(endpoint, crate::credentials::Credential::ApiKey { key }),
|
||||
(endpoint, codewhale_runtime::credentials::Credential::ApiKey { key }),
|
||||
);
|
||||
} else {
|
||||
mirror_saved_key_markers(&mut config, &provider_owned)?;
|
||||
@@ -830,7 +830,7 @@ pub(super) fn credential_writeability(
|
||||
underlying.session_provider_keys = Default::default();
|
||||
matches!(
|
||||
crate::config::resolve_credential_source(&underlying, identity).source,
|
||||
crate::credentials::CredentialSource::AccountSession
|
||||
codewhale_runtime::credentials::CredentialSource::AccountSession
|
||||
)
|
||||
} {
|
||||
return CredentialWriteability {
|
||||
@@ -957,7 +957,7 @@ pub(super) async fn clear_provider_key(
|
||||
.map_err(|error| ApiError::internal(format!("credential store: {error}")))?;
|
||||
let target = credential_store.as_mut().unwrap_or(&mut store);
|
||||
let slot = codewhale_config::credentials::provider_slot(kind);
|
||||
crate::credentials::store::with_provider_write_lock(slot, || {
|
||||
codewhale_runtime::credentials::store::with_provider_write_lock(slot, || {
|
||||
codewhale_config::credentials::clear_provider_api_key(target, &secrets, kind)
|
||||
})
|
||||
.map_err(|error| {
|
||||
@@ -1271,7 +1271,7 @@ mod tests {
|
||||
.api_key = Some("manual-key".into());
|
||||
*config.account_model_access.write() = Some(crate::config::AccountModelAccess {
|
||||
session_id: "current".into(),
|
||||
credential: crate::credentials::Credential::ApiKey {
|
||||
credential: codewhale_runtime::credentials::Credential::ApiKey {
|
||||
key: "cwc_fixture".into(),
|
||||
},
|
||||
expires_at: chrono::Utc::now().timestamp() + 60,
|
||||
|
||||
@@ -700,7 +700,7 @@ impl ProviderDashboardRow {
|
||||
let model_origin = ProviderModelOrigin::for_provider(provider, has_configured_model);
|
||||
// One sourced resolution per row: the picker must be able to say WHERE
|
||||
// it looked, not just that it found nothing (pi-mono's `AuthResult`
|
||||
// source, ported in `crate::credentials`).
|
||||
// source, ported in `codewhale_runtime::credentials`).
|
||||
let credential_resolution = crate::config::resolve_credential_source(config, &identity);
|
||||
let has_key = if provider == ProviderKind::Custom {
|
||||
custom_provider_has_auth(configured)
|
||||
@@ -711,7 +711,7 @@ impl ProviderDashboardRow {
|
||||
// the exact path is disclosed only inside the explicit reuse
|
||||
// confirmation.
|
||||
let credential_source = match &credential_resolution.source {
|
||||
crate::credentials::CredentialSource::ExternalGrant { cli, .. } => {
|
||||
codewhale_runtime::credentials::CredentialSource::ExternalGrant { cli, .. } => {
|
||||
format!("{cli} credentials (read-only)")
|
||||
}
|
||||
// Name the signed-in subscription account (label only) so two
|
||||
@@ -721,7 +721,7 @@ impl ProviderDashboardRow {
|
||||
// that structural read (`usable_sign_in`, formerly
|
||||
// `credentials_present`) still runs synchronously when a row is
|
||||
// built, as it did before #6715; naming the account adds no read.
|
||||
crate::credentials::CredentialSource::OAuth {
|
||||
codewhale_runtime::credentials::CredentialSource::OAuth {
|
||||
account: Some(account),
|
||||
..
|
||||
} => format!("{} · {account}", credential_resolution.source.label()),
|
||||
@@ -1602,7 +1602,7 @@ fn missing_auth_message(
|
||||
provider: ProviderKind,
|
||||
configured: Option<&crate::config::ProviderConfig>,
|
||||
provider_id: &str,
|
||||
resolution: &crate::credentials::CredentialResolution,
|
||||
resolution: &codewhale_runtime::credentials::CredentialResolution,
|
||||
) -> String {
|
||||
if provider == ProviderKind::Moonshot
|
||||
&& configured.is_some_and(crate::config::provider_config_uses_kimi_imported_token)
|
||||
|
||||
@@ -16,7 +16,7 @@ the licence obligation travels with the code rather than with a package.
|
||||
- **Project:** pi-mono (<https://github.com/earendil-works/pi-mono>)
|
||||
- **Author:** Mario Zechner
|
||||
- **Licence:** MIT
|
||||
- **Used in:** `crates/tui/src/credentials/` and
|
||||
- **Used in:** `crates/runtime/src/credentials/` and
|
||||
`crates/tui/src/config/credential_resolve.rs`
|
||||
- **What was taken:** the design of `packages/ai/src/auth/` — one type-tagged
|
||||
credential per provider, `modify` as the only serialized write path, a single
|
||||
|
||||
@@ -116,7 +116,7 @@ Slices:
|
||||
### 0.10.9: [Track B only] P1: credentials into codewhale-secrets.
|
||||
|
||||
Slices:
|
||||
- Move crates/tui/src/credentials/ (a directory: mod.rs 138, source.rs 195, store.rs 306, context.rs 63 = 702 production lines, tests.rs 294) into codewhale-secrets. The sequencer wrote 'credentials.rs'. Its one outbound edge, doctor (structural_url_authority), moves down first in the same slice. A different file, crates/config/src/credentials.rs, already exists: resolve the name overlap before choosing the module path.
|
||||
- Move crates/runtime/src/credentials/ (a directory: mod.rs 138, source.rs 195, store.rs 306, context.rs 63 = 702 production lines, tests.rs 294) into codewhale-secrets. The sequencer wrote 'credentials.rs'. Its one outbound edge, doctor (structural_url_authority), moves down first in the same slice. A different file, crates/config/src/credentials.rs, already exists: resolve the name overlap before choosing the module path.
|
||||
|
||||
- **Entry gate:** 0.10.8 shipped. 0.10.4's test-support is in (credentials tests mutate env). credentials/ and doctor.rs are not dirty. codewhale-config already depends on secrets, so secrets cannot depend on config: confirm credentials uses nothing from tui config (the only crate:: ref found outside tests is doctor).
|
||||
- **Exit gate:** Slice gate: cargo test -p codewhale-secrets for credentials tests with N > 0. Config credential-resolution tests via dev-test.sh. Journey: store, read back, rotate a key; no secret printed in logs. Windows ACL paths compile on hosted CI. Release gate as usual.
|
||||
@@ -239,7 +239,7 @@ Record at every release, on one machine, at a named source commit with a dirty-f
|
||||
- **0.10.6**: 0.10.6 X1 [Track B]: fslock into crates/runtime. Move try_lock_file_exclusive plus RuntimeProcessOwnerLock::try_lock_exclusive/is_contention (runtime_threads.rs:6592, 6855) to crates/runtime. Seam-introducing, not pure rename. Blocked until runtime_threads.rs and its tests are committed. Visibility review in the commit message; revert immediately on any lock regression.
|
||||
- **0.10.7**: 0.10.7 X7 [Track B]: tool rules into execpolicy. Reconcile the moved set (design table: tool_denied, tool_matches_any_rule, requires_raw_shell, policy_tool_aliases; AutoReviewPolicy lives in core/authority/auto_review) then move to crates/execpolicy with re-exports. Approval trust boundary: no matching or visibility change. Blocked by dirty core/authority.rs.
|
||||
- **0.10.8**: 0.10.8 X4 [Track B]: schema_sanitize/schema_canonicalize production half into codewhale-models. Move schema_sanitize.rs lines 1-469 and schema_canonicalize.rs to codewhale-models (owns Tool). Tests stay in tui. No new package name. Assert byte-identical sanitized output on existing fixtures (KV-cache prefix).
|
||||
- **0.10.9**: 0.10.9 P1 [Track B]: credentials/ directory into codewhale-secrets. Move crates/tui/src/credentials/ (702 prod lines, 294 test) to codewhale-secrets after moving doctor::structural_url_authority down. Resolve overlap with crates/config/src/credentials.rs. No secret printed, no file-mode or redaction change; Windows CI green.
|
||||
- **0.10.9**: 0.10.9 P1 [Track B]: credentials/ directory into codewhale-secrets. Move crates/runtime/src/credentials/ (702 prod lines, 294 test) to codewhale-secrets after moving doctor::structural_url_authority down. Resolve overlap with crates/config/src/credentials.rs. No secret printed, no file-mode or redaction change; Windows CI green.
|
||||
- **0.10.10**: 0.10.10 P2 seam [Track B]: remove tui-typed defaults from LlmClient. Remove effective_max_output_tokens (route_budget) and effective_route_envelope (cost_status) from the trait, update implementors, re-point ToolError to codewhale_tools, move RetryPolicy to codewhale-config. No crate moved. Blocked on #6396 for cost_status/route_budget.
|
||||
- **0.10.11**: 0.10.11 P2 move [Track B]: error_taxonomy + LlmError/LlmClient/RetryConfig/with_retry into codewhale-models. After the seam, move error_taxonomy.rs (1,054) and llm_client/mod.rs (2,164) together via move-modules.py. 120 consumer files via alias/shim. Serialized forms and session logs unchanged.
|
||||
- **0.10.12**: 0.10.12-0.10.13 X8 [Track B]: FleetRole free functions, then agent-contract move. 0.10.12: convert impl FleetRole (tools/subagent/mod.rs:639) to free functions and resolve the duplicate ChildAuthority. 0.10.13: move the DTO contract (prefer an existing crate; name must not collide with codewhale-agent). Blocked by untracked SUBAGENT/ and fleet/ work and by X7.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
"_comment": "Ceiling for dead-code suppressions across crates/, counting both `#[allow(dead_code)]` and `#[expect(dead_code)]`. `total` is the sum and is the ratcheted figure. It may go down freely; raising it needs a reviewer to say why in the PR. Regenerate with: python3 scripts/check-dead-code-budget.py --update",
|
||||
"_issue": "https://github.com/codewhale-hq/CodeWhale/issues/4785",
|
||||
"_expect_blind_spot": "Until #6241 this gate counted only the `allow` spelling, so rewriting an allow as an expect lowered the number without removing any dead code. The ceiling was re-based to the true combined count when that was fixed; it is not a regression.",
|
||||
"total": 212,
|
||||
"total": 209,
|
||||
"allow_total": 132,
|
||||
"expect_total": 80,
|
||||
"expect_total": 77,
|
||||
"per_crate": {
|
||||
"cli": {
|
||||
"allow": 0,
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"runtime": {
|
||||
"allow": 3,
|
||||
"expect": 2
|
||||
"expect": 3
|
||||
},
|
||||
"tools": {
|
||||
"allow": 2,
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"tui": {
|
||||
"allow": 108,
|
||||
"expect": 77
|
||||
"expect": 73
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user