返回 CodeWhale
source.rs
根目录 / crates / tui / src / credentials / source.rs
1 //! Where a credential came from, and — when there isn't one — where we looked.
2 //!
3 //! Ported from pi-mono's `AuthResult { auth, env, source }`
4 //! (`packages/ai/src/auth/types.ts`, MIT, Copyright (c) 2025 Mario Zechner;
5 //! full notice in the parent module). pi returns a human-readable `source`
6 //! such as `"ANTHROPIC_API_KEY"`, `"OAuth"`, or `"~/.aws/credentials"` from
7 //! every resolution so a status surface can say which place won.
8 //!
9 //! CodeWhale adds the negative half, because that is where its picker was
10 //! useless: a failed resolution carries the ordered list of places that were
11 //! actually probed, so "missing key" can name them and say what would fix it.
12 //!
13 //! Nothing here holds secret material — only labels.
14
15 use std::borrow::Cow;
16
17 /// One place credential resolution looked, and what would put a credential
18 /// there. Labels only; never a value.
19 #[derive(Debug, Clone, PartialEq, Eq)]
20 pub(crate) struct CredentialProbe {
21 /// Where we looked, e.g. `"env DEEPSEEK_API_KEY"` or `"secret store slot \"deepseek\""`.
22 pub(crate) place: Cow<'static, str>,
23 /// What the user would do to make this place answer, if there is a
24 /// one-line answer. `None` when the place is informational only.
25 pub(crate) fix: Option<Cow<'static, str>>,
26 }
27
28 impl CredentialProbe {
29 pub(crate) fn with_fix(
30 place: impl Into<Cow<'static, str>>,
31 fix: impl Into<Cow<'static, str>>,
32 ) -> Self {
33 Self {
34 place: place.into(),
35 fix: Some(fix.into()),
36 }
37 }
38 }
39
40 /// The place a credential was resolved from, or the fact that no place had
41 /// one. This is the single value every readiness surface should render.
42 #[derive(Debug, Clone, PartialEq, Eq)]
43 pub(crate) enum CredentialSource {
44 /// `auth_mode = "none"`: the route intentionally sends no credential.
45 AuthModeNone,
46 /// A keyless self-hosted or loopback route.
47 KeylessRoute { base_url: String },
48 /// `--api-key` on the command line (or the dispatcher's source-marked
49 /// forward of it).
50 CliOverride,
51 /// `[providers.<table>] api_key`.
52 ProviderConfigApiKey { table: String },
53 /// `[providers.<table>] api_key_env = "<var>"`, resolved from `<var>`.
54 ProviderConfigEnv { var: String },
55 /// An ambient provider environment variable.
56 AmbientEnv { var: String },
57 /// CodeWhale's own durable secret store.
58 SecretStore { slot: String },
59 /// A read-only, explicitly consented credential file owned by another CLI.
60 ExternalGrant { cli: String, path: String },
61 /// A subscription OAuth sign-in: Codewhale-owned storage, or (xAI) a
62 /// consented Grok CLI import. `account` is the display label (email,
63 /// plan) of the account it signs in as, taken from the same read that
64 /// proved the sign-in usable; never token material.
65 OAuth {
66 flow: String,
67 account: Option<String>,
68 },
69 /// The user-global `~/.codewhale/config.toml`, consulted last so a key
70 /// saved there survives loading a workspace config.
71 UserGlobalConfig,
72 /// An expiring, process-only Codewhale account auth transform.
73 AccountSession,
74 /// Nothing had a credential. `probed` is in precedence order.
75 Missing { probed: Vec<CredentialProbe> },
76 }
77
78 impl CredentialSource {
79 pub(crate) fn is_present(&self) -> bool {
80 !matches!(self, Self::Missing { .. })
81 }
82
83 /// Short human-readable label, in pi's spirit: the name of the place, not
84 /// a sentence. Safe to render anywhere — never contains a secret.
85 pub(crate) fn label(&self) -> Cow<'static, str> {
86 match self {
87 Self::AuthModeNone => Cow::Borrowed("auth_mode = \"none\""),
88 Self::KeylessRoute { base_url } => Cow::Owned(format!(
89 "keyless route {}",
90 crate::doctor::structural_url_authority(base_url)
91 )),
92 Self::CliOverride => Cow::Borrowed("--api-key"),
93 Self::ProviderConfigApiKey { table } => Cow::Owned(format!("[{table}] api_key")),
94 Self::ProviderConfigEnv { var } => Cow::Owned(format!("api_key_env {var}")),
95 Self::AmbientEnv { var } => Cow::Owned(var.clone()),
96 Self::SecretStore { slot } => Cow::Owned(format!("secret store \"{slot}\"")),
97 Self::ExternalGrant { cli, path } => {
98 Cow::Owned(format!("{cli} credentials (read-only) {path}"))
99 }
100 Self::OAuth { flow, .. } => Cow::Owned(format!("{flow} OAuth")),
101 Self::AccountSession => Cow::Borrowed("Codewhale account"),
102 Self::UserGlobalConfig => Cow::Borrowed("~/.codewhale/config.toml api_key"),
103 Self::Missing { .. } => Cow::Borrowed("not found"),
104 }
105 }
106
107 /// The ordered places that were probed, for a failed resolution.
108 pub(crate) fn probed(&self) -> &[CredentialProbe] {
109 match self {
110 Self::Missing { probed } => probed,
111 _ => &[],
112 }
113 }
114 }
115
116 /// A resolution plus its source. Deliberately does not carry the credential:
117 /// readiness surfaces need the source, and the request path already has its
118 /// own resolver that returns the secret.
119 #[derive(Debug, Clone, PartialEq, Eq)]
120 pub(crate) struct CredentialResolution {
121 pub(crate) source: CredentialSource,
122 }
123
124 impl CredentialResolution {
125 pub(crate) fn found(source: CredentialSource) -> Self {
126 debug_assert!(source.is_present());
127 Self { source }
128 }
129
130 pub(crate) fn missing(probed: Vec<CredentialProbe>) -> Self {
131 Self {
132 source: CredentialSource::Missing { probed },
133 }
134 }
135
136 pub(crate) fn is_present(&self) -> bool {
137 self.source.is_present()
138 }
139
140 /// One line naming the places checked, for a status row. Empty when the
141 /// resolution succeeded.
142 pub(crate) fn checked_places(&self) -> String {
143 self.source
144 .probed()
145 .iter()
146 .map(|probe| probe.place.as_ref())
147 .collect::<Vec<_>>()
148 .join(", ")
149 }
150
151 /// The first actionable fix among the probed places, if any.
152 pub(crate) fn first_fix(&self) -> Option<&str> {
153 self.source
154 .probed()
155 .iter()
156 .find_map(|probe| probe.fix.as_deref())
157 }
158 }
159
160 #[cfg(test)]
161 mod private_label_tests {
162 use super::CredentialSource;
163 #[test]
164 fn keyless_label_omits_credentials_and_still_names_the_endpoint() {
165 for url in [
166 "https://user:label-s10-synthetic@gateway.invalid/v1",
167 "https://label-s10-synthetic@gateway.invalid/v1",
168 "https://gateway.invalid/key/label-s10-synthetic/v1",
169 "https://gateway.invalid/v1?token=label-s10-synthetic",
170 "https://gateway.invalid/v1#label-s10-synthetic",
171 ] {
172 let label = CredentialSource::KeylessRoute {
173 base_url: url.into(),
174 }
175 .label()
176 .into_owned();
177 assert_eq!(label, "keyless route https://gateway.invalid");
178 }
179 let label = CredentialSource::KeylessRoute {
180 base_url: "http://localhost:11434/v1".into(),
181 }
182 .label()
183 .into_owned();
184 assert_eq!(label, "keyless route http://localhost:11434");
185 for url in ["not a url", "file:///etc/passwd", ""] {
186 let label = CredentialSource::KeylessRoute {
187 base_url: url.into(),
188 }
189 .label()
190 .into_owned();
191 assert!(label.contains("configured value omitted"));
192 assert!(!label.contains("passwd"));
193 }
194 }
195 }
196
196 lines RUST