mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(auth): migrate OAuth token cache from legacy sprout-agent path
The sprout-agent → buzz-agent rename (d99ad131f) moved the OAuth cache
directory without migrating existing tokens. Users on the latest main
get a browser auth prompt on every agent spawn because the new path is
empty.
When the primary cache path has no token, check the equivalent path
under ~/.config/sprout-agent/oauth/ and copy it to the new location.
The copy makes the migration one-time; subsequent starts find the token
at the primary path.
Co-authored-by: Will Pfleger <wpfleger@block.xyz>
Signed-off-by: Will Pfleger <wpfleger@block.xyz>
This commit is contained in:
@@ -120,7 +120,17 @@ impl PkceOAuthTokenSource {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|e| AgentError::Llm(format!("oauth cache dir {parent:?}: {e}")))?;
|
||||
}
|
||||
let initial = read_cache(&cache_path);
|
||||
let initial = read_cache(&cache_path).or_else(|| {
|
||||
// One-time migration: check the legacy sprout-agent cache path.
|
||||
let legacy = legacy_cache_path(&cache_path)?;
|
||||
let token = read_cache(&legacy)?;
|
||||
// Copy to the new location so subsequent starts skip this fallback.
|
||||
if let Ok(body) = fs::read(&legacy) {
|
||||
let _ = fs::write(&cache_path, &body);
|
||||
}
|
||||
tracing::info!("migrated oauth cache from legacy path {legacy:?}");
|
||||
Some(token)
|
||||
});
|
||||
Ok(Arc::new(Self {
|
||||
cfg,
|
||||
http: Client::new(),
|
||||
@@ -303,6 +313,19 @@ fn read_cache(path: &PathBuf) -> Option<CachedToken> {
|
||||
serde_json::from_slice(&body).ok()
|
||||
}
|
||||
|
||||
/// Compute the legacy cache path under `~/.config/sprout-agent/oauth/` for
|
||||
/// a given primary path under `~/.config/buzz-agent/oauth/`. Returns `None`
|
||||
/// if the path doesn't contain the expected `buzz-agent/oauth` segment.
|
||||
fn legacy_cache_path(primary: &std::path::Path) -> Option<PathBuf> {
|
||||
let s = primary.to_str()?;
|
||||
let marker = "/buzz-agent/oauth/";
|
||||
if s.contains(marker) {
|
||||
Some(PathBuf::from(s.replacen(marker, "/sprout-agent/oauth/", 1)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a token-endpoint JSON response. Fails loudly when `access_token`
|
||||
/// is missing or empty — without this, a malformed server response would
|
||||
/// be cached and `bearer()` would silently return `""` until the entry
|
||||
@@ -549,4 +572,63 @@ mod tests {
|
||||
let v: Value = serde_json::from_str(r#"{"access_token":""}"#).unwrap();
|
||||
assert!(token_from_response(&v, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_cache_path_rewrites_buzz_agent_to_sprout_agent() {
|
||||
let primary = PathBuf::from("/home/user/.config/buzz-agent/oauth/databricks/abc123.json");
|
||||
let legacy = legacy_cache_path(&primary).unwrap();
|
||||
assert_eq!(
|
||||
legacy,
|
||||
PathBuf::from("/home/user/.config/sprout-agent/oauth/databricks/abc123.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_cache_path_returns_none_for_override_path() {
|
||||
// cache_dir_override paths won't contain the buzz-agent segment
|
||||
let primary = PathBuf::from("/tmp/test-cache/databricks/abc123.json");
|
||||
assert!(legacy_cache_path(&primary).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_cache_migration_copies_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
// Set up the legacy path with a cached token
|
||||
let legacy_dir = tmp.path().join("sprout-agent/oauth/databricks");
|
||||
fs::create_dir_all(&legacy_dir).unwrap();
|
||||
let token = CachedToken {
|
||||
access_token: "migrated-token".into(),
|
||||
refresh_token: Some("migrated-refresh".into()),
|
||||
expires_at: Some(9999999999),
|
||||
};
|
||||
let legacy_file = legacy_dir.join("hash.json");
|
||||
fs::write(&legacy_file, serde_json::to_vec(&token).unwrap()).unwrap();
|
||||
|
||||
// Primary path does not exist yet
|
||||
let primary_dir = tmp.path().join("buzz-agent/oauth/databricks");
|
||||
fs::create_dir_all(&primary_dir).unwrap();
|
||||
let primary_file = primary_dir.join("hash.json");
|
||||
assert!(!primary_file.exists());
|
||||
|
||||
// Simulate the migration: read_cache on primary fails, legacy_cache_path
|
||||
// rewrites, read_cache on legacy succeeds, file is copied.
|
||||
let initial = read_cache(&primary_file).or_else(|| {
|
||||
let legacy = legacy_cache_path(&primary_file)?;
|
||||
let tok = read_cache(&legacy)?;
|
||||
if let Ok(body) = fs::read(&legacy) {
|
||||
let _ = fs::write(&primary_file, &body);
|
||||
}
|
||||
Some(tok)
|
||||
});
|
||||
|
||||
let loaded = initial.unwrap();
|
||||
assert_eq!(loaded.access_token, "migrated-token");
|
||||
assert_eq!(loaded.refresh_token.as_deref(), Some("migrated-refresh"));
|
||||
// Verify the file was copied to the new location
|
||||
assert!(primary_file.exists());
|
||||
let copied: CachedToken =
|
||||
serde_json::from_slice(&fs::read(&primary_file).unwrap()).unwrap();
|
||||
assert_eq!(copied.access_token, "migrated-token");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user