1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//! Picks up a new login after the provider refused a session's credentials,
//! and drops credentials an ACP `logout` deleted.
//!
//! Providers capture their key or OAuth tokens when built, so a prompt that
//! fails with `auth_required` leaves the session holding the refused ones,
//! and a logout leaves it holding deleted ones. The next prompt rebuilds the
//! provider from the credential store, so a host's sign-in (or a
//! `rho --login` elsewhere) applies without reloading, and a logout fails it
//! with `auth_required` as missing credentials do.
use std::sync::Arc;
use super::{model_scoped_config, SessionHost};
use crate::{
app::acp::{
auth::CredentialFailure,
auth_logout::{LogoutCount, Logouts},
},
config::Config,
credential_store::{build_provider_from_config_ensuring_catalog, AppCredentialStore},
};
/// Whether the session's provider still holds credentials the store has.
#[derive(Clone, Debug)]
pub(super) struct SessionCredentials {
logouts: Logouts,
state: CredentialState,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CredentialState {
/// Read from the store when [`Logouts`] stood at this count; stale once
/// a later logout finishes.
Read(LogoutCount),
/// A prompt failed on a credential failure; reacquire before the next.
Refused,
}
impl SessionCredentials {
/// For a provider that has just read credentials. Session builds hold
/// the agent's install gate, which `logout` waits out, so none reads
/// credentials before a logout and counts after it.
pub(super) fn just_read(logouts: &Logouts) -> Self {
Self {
state: CredentialState::Read(logouts.count()),
logouts: logouts.clone(),
}
}
/// Take before building a provider and pass to [`SessionHost::switch_provider`],
/// so a logout during the build still counts as after it.
pub(super) fn before_read(&self) -> LogoutCount {
self.logouts.count()
}
pub(super) fn mark_read(&mut self, read: LogoutCount) {
self.state = CredentialState::Read(read);
}
fn stale(&self) -> bool {
match self.state {
CredentialState::Read(read) => self.logouts.since(read),
CredentialState::Refused => true,
}
}
}
impl SessionHost {
/// Rebuilds a stale provider for the same model and auth. Still-missing
/// credentials fail with the typed provider error, which the agent maps
/// to `auth_required` again.
pub(super) async fn renew_stale_credentials(
&mut self,
process_config: &Config,
) -> anyhow::Result<()> {
if !self.credentials.stale() {
return Ok(());
}
let read = self.credentials.before_read();
let mut config = model_scoped_config(&self.current_model(), process_config);
config.reasoning = self.built.session.reasoning_level();
// The login that fixes it ran in another process, such as the
// editor's terminal login.
AppCredentialStore::reread_external_logins();
let provider =
build_provider_from_config_ensuring_catalog(&config, Arc::new(AppCredentialStore))
.await?;
// The model switch path also rebinds the compactor, which holds its
// own provider. The model is unchanged, so no switch notice is saved.
self.switch_provider(provider, &config, read)
.map_err(|error| match error.data {
Some(serde_json::Value::String(detail)) => {
anyhow::anyhow!("could not renew credentials: {detail}")
}
_ => anyhow::anyhow!("could not renew credentials: {}", error.message),
})?;
Ok(())
}
/// Marks the provider stale when `result` failed on its credentials.
pub(super) fn note_credential_failure<T>(&mut self, result: &anyhow::Result<T>) {
if result
.as_ref()
.is_err_and(|error| CredentialFailure::find(error).is_some())
{
self.credentials.state = CredentialState::Refused;
}
}
}