Enable saved OAuth startup auth without breaking local version output

Startup auth was split between the CLI and API crates, which made saved OAuth refresh behavior eager and easy to drift. This change adds a startup-specific resolver in the API layer, keeps env-only auth semantics intact, preserves saved refresh tokens when refresh responses omit them, and lets the CLI reuse the shared resolver while keeping --version on a purely local path.

Constraint: Saved OAuth credentials live in ~/.claude/credentials.json and must remain compatible with existing runtime helpers
Constraint: --version must not require config loading or any API/auth client initialization
Rejected: Keep refresh orchestration only in rusty-claude-cli | would preserve split auth policy and lazy-load bugs
Rejected: Change AnthropicClient::from_env to load config | would broaden configless API semantics for non-CLI callers
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep startup-only OAuth refresh separate from AuthSource::from_env() / AnthropicClient::from_env() unless all non-CLI callers are re-evaluated
Tested: cargo fmt --all; cargo build; cargo clippy --workspace --all-targets -- -D warnings; cargo test; cargo run -p rusty-claude-cli -- --version
Not-tested: Live OAuth refresh against a real auth server
This commit is contained in:
Yeachan-Heo
2026-04-01 00:24:55 +00:00
parent c139fe9bee
commit 9455280f24
3 changed files with 162 additions and 26 deletions

View File

@@ -11,7 +11,7 @@ use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use api::{
resolve_saved_oauth_token, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock,
resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock,
InputMessage, MessageRequest, MessageResponse, OutputContentBlock,
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
};
@@ -1878,20 +1878,13 @@ impl AnthropicRuntimeClient {
}
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
match AuthSource::from_env() {
Ok(auth) => Ok(auth),
Err(api::ApiError::MissingApiKey) => {
let cwd = env::current_dir()?;
let config = ConfigLoader::default_for(&cwd).load()?;
if let Some(oauth) = config.oauth() {
if let Some(token_set) = resolve_saved_oauth_token(oauth)? {
return Ok(AuthSource::from(token_set));
}
}
Ok(AuthSource::from_env_or_saved()?)
}
Err(error) => Err(Box::new(error)),
}
Ok(resolve_startup_auth_source(|| {
let cwd = env::current_dir().map_err(api::ApiError::from)?;
let config = ConfigLoader::default_for(&cwd).load().map_err(|error| {
api::ApiError::Auth(format!("failed to load runtime OAuth config: {error}"))
})?;
Ok(config.oauth().cloned())
})?)
}
impl ApiClient for AnthropicRuntimeClient {