返回 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 /// The root `api_key` compatibility slot in the config file.
52 RootConfigApiKey,
53 /// `[providers.<table>] api_key`.
54 ProviderConfigApiKey { table: String },
55 /// `[providers.<table>] api_key_env = "<var>"`, resolved from `<var>`.
56 ProviderConfigEnv { var: String },
57 /// An ambient provider environment variable.
58 AmbientEnv { var: String },
59 /// CodeWhale's own durable secret store.
60 SecretStore { slot: String },
61 /// A read-only, explicitly consented credential file owned by another CLI.
62 ExternalGrant { cli: String, path: String },
63 /// CodeWhale-owned OAuth device-login storage (xAI today).
64 OAuth { flow: String },
65 /// The user-global `~/.codewhale/config.toml`, consulted last so a key
66 /// saved there survives loading a workspace config.
67 UserGlobalConfig,
68 /// An expiring, process-only Codewhale account auth transform.
69 AccountSession,
70 /// Nothing had a credential. `probed` is in precedence order.
71 Missing { probed: Vec<CredentialProbe> },
72 }
73
74 impl CredentialSource {
75 pub(crate) fn is_present(&self) -> bool {
76 !matches!(self, Self::Missing { .. })
77 }
78
79 /// Short human-readable label, in pi's spirit: the name of the place, not
80 /// a sentence. Safe to render anywhere — never contains a secret.
81 pub(crate) fn label(&self) -> Cow<'static, str> {
82 match self {
83 Self::AuthModeNone => Cow::Borrowed("auth_mode = \"none\""),
84 Self::KeylessRoute { base_url } => Cow::Owned(format!("keyless route {base_url}")),
85 Self::CliOverride => Cow::Borrowed("--api-key"),
86 Self::RootConfigApiKey => Cow::Borrowed("config api_key"),
87 Self::ProviderConfigApiKey { table } => Cow::Owned(format!("[{table}] api_key")),
88 Self::ProviderConfigEnv { var } => Cow::Owned(format!("api_key_env {var}")),
89 Self::AmbientEnv { var } => Cow::Owned(var.clone()),
90 Self::SecretStore { slot } => Cow::Owned(format!("secret store \"{slot}\"")),
91 Self::ExternalGrant { cli, path } => {
92 Cow::Owned(format!("{cli} credentials (read-only) {path}"))
93 }
94 Self::OAuth { flow } => Cow::Owned(format!("{flow} OAuth")),
95 Self::AccountSession => Cow::Borrowed("Codewhale account"),
96 Self::UserGlobalConfig => Cow::Borrowed("~/.codewhale/config.toml api_key"),
97 Self::Missing { .. } => Cow::Borrowed("not found"),
98 }
99 }
100
101 /// The ordered places that were probed, for a failed resolution.
102 pub(crate) fn probed(&self) -> &[CredentialProbe] {
103 match self {
104 Self::Missing { probed } => probed,
105 _ => &[],
106 }
107 }
108 }
109
110 /// A resolution plus its source. Deliberately does not carry the credential:
111 /// readiness surfaces need the source, and the request path already has its
112 /// own resolver that returns the secret.
113 #[derive(Debug, Clone, PartialEq, Eq)]
114 pub(crate) struct CredentialResolution {
115 pub(crate) source: CredentialSource,
116 }
117
118 impl CredentialResolution {
119 pub(crate) fn found(source: CredentialSource) -> Self {
120 debug_assert!(source.is_present());
121 Self { source }
122 }
123
124 pub(crate) fn missing(probed: Vec<CredentialProbe>) -> Self {
125 Self {
126 source: CredentialSource::Missing { probed },
127 }
128 }
129
130 pub(crate) fn is_present(&self) -> bool {
131 self.source.is_present()
132 }
133
134 /// One line naming the places checked, for a status row. Empty when the
135 /// resolution succeeded.
136 pub(crate) fn checked_places(&self) -> String {
137 self.source
138 .probed()
139 .iter()
140 .map(|probe| probe.place.as_ref())
141 .collect::<Vec<_>>()
142 .join(", ")
143 }
144
145 /// The first actionable fix among the probed places, if any.
146 pub(crate) fn first_fix(&self) -> Option<&str> {
147 self.source
148 .probed()
149 .iter()
150 .find_map(|probe| probe.fix.as_deref())
151 }
152 }
153
153 lines RUST