mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-12 19:14:51 +08:00
Remove the deprecated Claude subscription login path and restore a green Rust workspace
ROADMAP #37 was still open even though several earlier backlog items were already closed. This change removes the local login/logout surface, stops startup auth resolution from treating saved OAuth credentials as a supported path, and updates diagnostics/help to point users at ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN only. While proving the change with the user-requested workspace gates, clippy surfaced additional pre-existing warning failures across the Rust workspace. Those were cleaned up in-place so the required `cargo fmt`, `cargo clippy --workspace --all-targets -- -D warnings`, and `cargo test --workspace` sequence now passes end to end. Constraint: User explicitly required full-workspace fmt/clippy/test before commit/push Constraint: Existing dirty leader worktree had to be stashed before attempted OMX team worktree launch Rejected: Keep login/logout but hide them from help | left unsupported auth flow and saved OAuth fallback intact Rejected: Stop after ROADMAP #37 targeted tests | did not satisfy required full-workspace verification gate Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Do not reintroduce saved OAuth as a silent Anthropic startup fallback without an explicit supported auth policy Tested: cargo fmt --all --check; cargo clippy --workspace --all-targets -- -D warnings; cargo test --workspace Not-tested: Remote push effects beyond origin/main update
This commit is contained in:
@@ -14,14 +14,13 @@ fn main() {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string());
|
||||
|
||||
println!("cargo:rustc-env=GIT_SHA={}", git_sha);
|
||||
println!("cargo:rustc-env=GIT_SHA={git_sha}");
|
||||
|
||||
// TARGET is always set by Cargo during build
|
||||
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
|
||||
println!("cargo:rustc-env=TARGET={}", target);
|
||||
println!("cargo:rustc-env=TARGET={target}");
|
||||
|
||||
// Build date from SOURCE_DATE_EPOCH (reproducible builds) or current UTC date.
|
||||
// Intentionally ignoring time component to keep output deterministic within a day.
|
||||
@@ -48,8 +47,7 @@ fn main() {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
|
||||
});
|
||||
println!("cargo:rustc-env=BUILD_DATE={build_date}");
|
||||
|
||||
|
||||
@@ -24,11 +24,10 @@ use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||
|
||||
use api::{
|
||||
detect_provider_kind, oauth_token_is_expired, resolve_startup_auth_source, AnthropicClient,
|
||||
AuthSource, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest,
|
||||
MessageResponse, OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient,
|
||||
ProviderKind, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition,
|
||||
ToolResultContentBlock,
|
||||
detect_provider_kind, resolve_startup_auth_source, AnthropicClient, AuthSource,
|
||||
ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest, MessageResponse,
|
||||
OutputContentBlock, PromptCache, ProviderClient as ApiProviderClient, ProviderKind,
|
||||
StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock,
|
||||
};
|
||||
|
||||
use commands::{
|
||||
@@ -43,15 +42,13 @@ use init::initialize_repo;
|
||||
use plugins::{PluginHooks, PluginManager, PluginManagerConfig, PluginRegistry};
|
||||
use render::{MarkdownStreamState, Spinner, TerminalRenderer};
|
||||
use runtime::{
|
||||
check_base_commit, clear_oauth_credentials, format_stale_base_warning, format_usd,
|
||||
generate_pkce_pair, generate_state, load_oauth_credentials, load_system_prompt,
|
||||
parse_oauth_callback_request_target, pricing_for_model, resolve_expected_base,
|
||||
resolve_sandbox_status, save_oauth_credentials, ApiClient, ApiRequest, AssistantEvent,
|
||||
CompactionConfig, ConfigLoader, ConfigSource, ContentBlock, ConversationMessage,
|
||||
ConversationRuntime, McpServer, McpServerManager, McpServerSpec, McpTool, MessageRole,
|
||||
ModelPricing, OAuthAuthorizationRequest, OAuthConfig, OAuthTokenExchangeRequest,
|
||||
PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent, ResolvedPermissionMode,
|
||||
RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
|
||||
check_base_commit, format_stale_base_warning, format_usd, load_oauth_credentials,
|
||||
load_system_prompt, pricing_for_model, resolve_expected_base, resolve_sandbox_status,
|
||||
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource,
|
||||
ContentBlock, ConversationMessage, ConversationRuntime, McpServer, McpServerManager,
|
||||
McpServerSpec, McpTool, MessageRole, ModelPricing, PermissionMode, PermissionPolicy,
|
||||
ProjectContext, PromptCacheEvent, ResolvedPermissionMode, RuntimeError, Session, TokenUsage,
|
||||
ToolError, ToolExecutor, UsageTracker,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -244,8 +241,6 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
cli.set_reasoning_effort(reasoning_effort);
|
||||
cli.run_turn_with_output(&effective_prompt, output_format, compact)?;
|
||||
}
|
||||
CliAction::Login { output_format } => run_login(output_format)?,
|
||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||
CliAction::Doctor { output_format } => run_doctor(output_format)?,
|
||||
CliAction::State { output_format } => run_worker_state(output_format)?,
|
||||
CliAction::Init { output_format } => run_init(output_format)?,
|
||||
@@ -332,12 +327,6 @@ enum CliAction {
|
||||
reasoning_effort: Option<String>,
|
||||
allow_broad_cwd: bool,
|
||||
},
|
||||
Login {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Logout {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
Doctor {
|
||||
output_format: CliOutputFormat,
|
||||
},
|
||||
@@ -418,8 +407,6 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
&& matches!(
|
||||
rest[0].as_str(),
|
||||
"prompt"
|
||||
| "login"
|
||||
| "logout"
|
||||
| "version"
|
||||
| "state"
|
||||
| "init"
|
||||
@@ -667,8 +654,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||
}
|
||||
}
|
||||
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
|
||||
"login" => Ok(CliAction::Login { output_format }),
|
||||
"logout" => Ok(CliAction::Logout { output_format }),
|
||||
"login" | "logout" => Err(removed_auth_surface_error(rest[0].as_str())),
|
||||
"init" => Ok(CliAction::Init { output_format }),
|
||||
"export" => parse_export_args(&rest[1..], output_format),
|
||||
"prompt" => {
|
||||
@@ -765,8 +751,6 @@ fn bare_slash_command_guidance(command_name: &str) -> Option<String> {
|
||||
| "mcp"
|
||||
| "skills"
|
||||
| "system-prompt"
|
||||
| "login"
|
||||
| "logout"
|
||||
| "init"
|
||||
| "prompt"
|
||||
| "export"
|
||||
@@ -788,12 +772,19 @@ fn bare_slash_command_guidance(command_name: &str) -> Option<String> {
|
||||
Some(guidance)
|
||||
}
|
||||
|
||||
fn removed_auth_surface_error(command_name: &str) -> String {
|
||||
format!(
|
||||
"`claw {command_name}` has been removed. Set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN instead."
|
||||
)
|
||||
}
|
||||
|
||||
fn join_optional_args(args: &[String]) -> Option<String> {
|
||||
let joined = args.join(" ");
|
||||
let trimmed = joined.trim();
|
||||
(!trimmed.is_empty()).then(|| trimmed.to_string())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::needless_pass_by_value)]
|
||||
fn parse_direct_slash_cli_action(
|
||||
rest: &[String],
|
||||
model: String,
|
||||
@@ -1168,7 +1159,7 @@ fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result<
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| "missing value for --session".to_string())?;
|
||||
session_reference = value.clone();
|
||||
session_reference.clone_from(value);
|
||||
index += 2;
|
||||
}
|
||||
flag if flag.starts_with("--session=") => {
|
||||
@@ -1529,79 +1520,65 @@ fn check_auth_health() -> DiagnosticCheck {
|
||||
let auth_token_present = env::var("ANTHROPIC_AUTH_TOKEN")
|
||||
.ok()
|
||||
.is_some_and(|value| !value.trim().is_empty());
|
||||
let env_details = format!(
|
||||
"Environment api_key={} auth_token={}",
|
||||
if api_key_present { "present" } else { "absent" },
|
||||
if auth_token_present {
|
||||
"present"
|
||||
} else {
|
||||
"absent"
|
||||
}
|
||||
);
|
||||
|
||||
match load_oauth_credentials() {
|
||||
Ok(Some(token_set)) => {
|
||||
let expired = oauth_token_is_expired(&api::OAuthTokenSet {
|
||||
access_token: token_set.access_token.clone(),
|
||||
refresh_token: token_set.refresh_token.clone(),
|
||||
expires_at: token_set.expires_at,
|
||||
scopes: token_set.scopes.clone(),
|
||||
});
|
||||
let mut details = vec![
|
||||
format!(
|
||||
"Environment api_key={} auth_token={}",
|
||||
if api_key_present { "present" } else { "absent" },
|
||||
if auth_token_present {
|
||||
"present"
|
||||
} else {
|
||||
"absent"
|
||||
}
|
||||
),
|
||||
format!(
|
||||
"Saved OAuth expires_at={} refresh_token={} scopes={}",
|
||||
token_set
|
||||
.expires_at
|
||||
.map_or_else(|| "<none>".to_string(), |value| value.to_string()),
|
||||
if token_set.refresh_token.is_some() {
|
||||
"present"
|
||||
} else {
|
||||
"absent"
|
||||
},
|
||||
if token_set.scopes.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
token_set.scopes.join(",")
|
||||
}
|
||||
),
|
||||
];
|
||||
if expired {
|
||||
details.push(
|
||||
"Suggested action claw login to refresh local OAuth credentials".to_string(),
|
||||
);
|
||||
}
|
||||
DiagnosticCheck::new(
|
||||
"Auth",
|
||||
if expired {
|
||||
DiagnosticLevel::Warn
|
||||
Ok(Some(token_set)) => DiagnosticCheck::new(
|
||||
"Auth",
|
||||
if api_key_present || auth_token_present {
|
||||
DiagnosticLevel::Ok
|
||||
} else {
|
||||
DiagnosticLevel::Warn
|
||||
},
|
||||
if api_key_present || auth_token_present {
|
||||
"supported auth env vars are configured; legacy saved OAuth is ignored"
|
||||
} else {
|
||||
"legacy saved OAuth credentials are present but unsupported"
|
||||
},
|
||||
)
|
||||
.with_details(vec![
|
||||
env_details,
|
||||
format!(
|
||||
"Legacy OAuth expires_at={} refresh_token={} scopes={}",
|
||||
token_set
|
||||
.expires_at
|
||||
.map_or_else(|| "<none>".to_string(), |value| value.to_string()),
|
||||
if token_set.refresh_token.is_some() {
|
||||
"present"
|
||||
} else {
|
||||
DiagnosticLevel::Ok
|
||||
"absent"
|
||||
},
|
||||
if expired {
|
||||
"saved OAuth credentials are present but expired"
|
||||
} else if api_key_present || auth_token_present {
|
||||
"environment and saved credentials are available"
|
||||
if token_set.scopes.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
"saved OAuth credentials are available"
|
||||
},
|
||||
)
|
||||
.with_details(details)
|
||||
.with_data(Map::from_iter([
|
||||
("api_key_present".to_string(), json!(api_key_present)),
|
||||
("auth_token_present".to_string(), json!(auth_token_present)),
|
||||
("saved_oauth_present".to_string(), json!(true)),
|
||||
("saved_oauth_expired".to_string(), json!(expired)),
|
||||
(
|
||||
"saved_oauth_expires_at".to_string(),
|
||||
json!(token_set.expires_at),
|
||||
),
|
||||
(
|
||||
"refresh_token_present".to_string(),
|
||||
json!(token_set.refresh_token.is_some()),
|
||||
),
|
||||
("scopes".to_string(), json!(token_set.scopes)),
|
||||
]))
|
||||
}
|
||||
token_set.scopes.join(",")
|
||||
}
|
||||
),
|
||||
"Suggested action set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN; `claw login` is removed"
|
||||
.to_string(),
|
||||
])
|
||||
.with_data(Map::from_iter([
|
||||
("api_key_present".to_string(), json!(api_key_present)),
|
||||
("auth_token_present".to_string(), json!(auth_token_present)),
|
||||
("legacy_saved_oauth_present".to_string(), json!(true)),
|
||||
(
|
||||
"legacy_saved_oauth_expires_at".to_string(),
|
||||
json!(token_set.expires_at),
|
||||
),
|
||||
(
|
||||
"legacy_refresh_token_present".to_string(),
|
||||
json!(token_set.refresh_token.is_some()),
|
||||
),
|
||||
("legacy_scopes".to_string(), json!(token_set.scopes)),
|
||||
])),
|
||||
Ok(None) => DiagnosticCheck::new(
|
||||
"Auth",
|
||||
if api_key_present || auth_token_present {
|
||||
@@ -1610,43 +1587,33 @@ fn check_auth_health() -> DiagnosticCheck {
|
||||
DiagnosticLevel::Warn
|
||||
},
|
||||
if api_key_present || auth_token_present {
|
||||
"environment credentials are configured"
|
||||
"supported auth env vars are configured"
|
||||
} else {
|
||||
"no API key or saved OAuth credentials were found"
|
||||
"no supported auth env vars were found"
|
||||
},
|
||||
)
|
||||
.with_details(vec![format!(
|
||||
"Environment api_key={} auth_token={}",
|
||||
if api_key_present { "present" } else { "absent" },
|
||||
if auth_token_present {
|
||||
"present"
|
||||
} else {
|
||||
"absent"
|
||||
}
|
||||
)])
|
||||
.with_details(vec![env_details])
|
||||
.with_data(Map::from_iter([
|
||||
("api_key_present".to_string(), json!(api_key_present)),
|
||||
("auth_token_present".to_string(), json!(auth_token_present)),
|
||||
("saved_oauth_present".to_string(), json!(false)),
|
||||
("saved_oauth_expired".to_string(), json!(false)),
|
||||
("saved_oauth_expires_at".to_string(), Value::Null),
|
||||
("refresh_token_present".to_string(), json!(false)),
|
||||
("scopes".to_string(), json!(Vec::<String>::new())),
|
||||
("legacy_saved_oauth_present".to_string(), json!(false)),
|
||||
("legacy_saved_oauth_expires_at".to_string(), Value::Null),
|
||||
("legacy_refresh_token_present".to_string(), json!(false)),
|
||||
("legacy_scopes".to_string(), json!(Vec::<String>::new())),
|
||||
])),
|
||||
Err(error) => DiagnosticCheck::new(
|
||||
"Auth",
|
||||
DiagnosticLevel::Fail,
|
||||
format!("failed to inspect saved credentials: {error}"),
|
||||
format!("failed to inspect legacy saved credentials: {error}"),
|
||||
)
|
||||
.with_data(Map::from_iter([
|
||||
("api_key_present".to_string(), json!(api_key_present)),
|
||||
("auth_token_present".to_string(), json!(auth_token_present)),
|
||||
("saved_oauth_present".to_string(), Value::Null),
|
||||
("saved_oauth_expired".to_string(), Value::Null),
|
||||
("saved_oauth_expires_at".to_string(), Value::Null),
|
||||
("refresh_token_present".to_string(), Value::Null),
|
||||
("scopes".to_string(), Value::Null),
|
||||
("saved_oauth_error".to_string(), json!(error.to_string())),
|
||||
("legacy_saved_oauth_present".to_string(), Value::Null),
|
||||
("legacy_saved_oauth_expires_at".to_string(), Value::Null),
|
||||
("legacy_refresh_token_present".to_string(), Value::Null),
|
||||
("legacy_scopes".to_string(), Value::Null),
|
||||
("legacy_saved_oauth_error".to_string(), json!(error.to_string())),
|
||||
])),
|
||||
}
|
||||
}
|
||||
@@ -1993,182 +1960,6 @@ fn print_bootstrap_plan(output_format: CliOutputFormat) -> Result<(), Box<dyn st
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_oauth_config() -> OAuthConfig {
|
||||
OAuthConfig {
|
||||
client_id: String::from("9d1c250a-e61b-44d9-88ed-5944d1962f5e"),
|
||||
authorize_url: String::from("https://platform.claude.com/oauth/authorize"),
|
||||
token_url: String::from("https://platform.claude.com/v1/oauth/token"),
|
||||
callback_port: None,
|
||||
manual_redirect_url: None,
|
||||
scopes: vec![
|
||||
String::from("user:profile"),
|
||||
String::from("user:inference"),
|
||||
String::from("user:sessions:claude_code"),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
let config = ConfigLoader::default_for(&cwd).load()?;
|
||||
let default_oauth = default_oauth_config();
|
||||
let oauth = config.oauth().unwrap_or(&default_oauth);
|
||||
let callback_port = oauth.callback_port.unwrap_or(DEFAULT_OAUTH_CALLBACK_PORT);
|
||||
let redirect_uri = runtime::loopback_redirect_uri(callback_port);
|
||||
let pkce = generate_pkce_pair()?;
|
||||
let state = generate_state()?;
|
||||
let authorize_url =
|
||||
OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
|
||||
.build_url();
|
||||
|
||||
if output_format == CliOutputFormat::Text {
|
||||
println!("Starting Claude OAuth login...");
|
||||
println!("Listening for callback on {redirect_uri}");
|
||||
}
|
||||
if let Err(error) = open_browser(&authorize_url) {
|
||||
emit_login_browser_open_failure(
|
||||
output_format,
|
||||
&authorize_url,
|
||||
&error,
|
||||
&mut io::stdout(),
|
||||
&mut io::stderr(),
|
||||
)?;
|
||||
}
|
||||
|
||||
let callback = wait_for_oauth_callback(callback_port)?;
|
||||
if let Some(error) = callback.error {
|
||||
let description = callback
|
||||
.error_description
|
||||
.unwrap_or_else(|| "authorization failed".to_string());
|
||||
return Err(io::Error::other(format!("{error}: {description}")).into());
|
||||
}
|
||||
let code = callback.code.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "callback did not include code")
|
||||
})?;
|
||||
let returned_state = callback.state.ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "callback did not include state")
|
||||
})?;
|
||||
if returned_state != state {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "oauth state mismatch").into());
|
||||
}
|
||||
|
||||
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
|
||||
let exchange_request = OAuthTokenExchangeRequest::from_config(
|
||||
oauth,
|
||||
code,
|
||||
state,
|
||||
pkce.verifier,
|
||||
redirect_uri.clone(),
|
||||
);
|
||||
let runtime = tokio::runtime::Runtime::new()?;
|
||||
let token_set = runtime.block_on(client.exchange_oauth_code(oauth, &exchange_request))?;
|
||||
save_oauth_credentials(&runtime::OAuthTokenSet {
|
||||
access_token: token_set.access_token,
|
||||
refresh_token: token_set.refresh_token,
|
||||
expires_at: token_set.expires_at,
|
||||
scopes: token_set.scopes,
|
||||
})?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("Claude OAuth login complete."),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "login",
|
||||
"callback_port": callback_port,
|
||||
"redirect_uri": redirect_uri,
|
||||
"message": "Claude OAuth login complete.",
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_login_browser_open_failure(
|
||||
output_format: CliOutputFormat,
|
||||
authorize_url: &str,
|
||||
error: &io::Error,
|
||||
stdout: &mut impl Write,
|
||||
stderr: &mut impl Write,
|
||||
) -> io::Result<()> {
|
||||
writeln!(
|
||||
stderr,
|
||||
"warning: failed to open browser automatically: {error}"
|
||||
)?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => writeln!(stdout, "Open this URL manually:\n{authorize_url}"),
|
||||
CliOutputFormat::Json => writeln!(stderr, "Open this URL manually:\n{authorize_url}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
clear_oauth_credentials()?;
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("Claude OAuth credentials cleared."),
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&json!({
|
||||
"kind": "logout",
|
||||
"message": "Claude OAuth credentials cleared.",
|
||||
}))?
|
||||
),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_browser(url: &str) -> io::Result<()> {
|
||||
let commands = if cfg!(target_os = "macos") {
|
||||
vec![("open", vec![url])]
|
||||
} else if cfg!(target_os = "windows") {
|
||||
vec![("cmd", vec!["/C", "start", "", url])]
|
||||
} else {
|
||||
vec![("xdg-open", vec![url])]
|
||||
};
|
||||
for (program, args) in commands {
|
||||
match Command::new(program).args(args).spawn() {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"no supported browser opener command found",
|
||||
))
|
||||
}
|
||||
|
||||
fn wait_for_oauth_callback(
|
||||
port: u16,
|
||||
) -> Result<runtime::OAuthCallbackParams, Box<dyn std::error::Error>> {
|
||||
let listener = TcpListener::bind(("127.0.0.1", port))?;
|
||||
let (mut stream, _) = listener.accept()?;
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let bytes_read = stream.read(&mut buffer)?;
|
||||
let request = String::from_utf8_lossy(&buffer[..bytes_read]);
|
||||
let request_line = request.lines().next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, "missing callback request line")
|
||||
})?;
|
||||
let target = request_line.split_whitespace().nth(1).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"missing callback request target",
|
||||
)
|
||||
})?;
|
||||
let callback = parse_oauth_callback_request_target(target)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
let body = if callback.error.is_some() {
|
||||
"Claude OAuth login failed. You can close this window."
|
||||
} else {
|
||||
"Claude OAuth login succeeded. You can close this window."
|
||||
};
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream.write_all(response.as_bytes())?;
|
||||
Ok(callback)
|
||||
}
|
||||
|
||||
fn print_system_prompt(
|
||||
cwd: PathBuf,
|
||||
date: String,
|
||||
@@ -2214,6 +2005,7 @@ fn version_json_value() -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
|
||||
let session_reference = session_path.display().to_string();
|
||||
let (handle, session) = match load_session_reference(&session_reference) {
|
||||
@@ -3036,8 +2828,7 @@ fn detect_broad_cwd() -> Option<PathBuf> {
|
||||
};
|
||||
let is_home = env::var_os("HOME")
|
||||
.or_else(|| env::var_os("USERPROFILE"))
|
||||
.map(|h| PathBuf::from(h) == cwd)
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|h| Path::new(&h) == cwd);
|
||||
let is_root = cwd.parent().is_none();
|
||||
if is_home || is_root {
|
||||
Some(cwd)
|
||||
@@ -3109,9 +2900,8 @@ fn enforce_broad_cwd_policy(
|
||||
}
|
||||
|
||||
fn run_stale_base_preflight(flag_value: Option<&str>) {
|
||||
let cwd = match env::current_dir() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(_) => return,
|
||||
let Ok(cwd) = env::current_dir() else {
|
||||
return;
|
||||
};
|
||||
let source = resolve_expected_base(flag_value, &cwd);
|
||||
let state = check_base_commit(&cwd, source.as_ref());
|
||||
@@ -3120,6 +2910,7 @@ fn run_stale_base_preflight(flag_value: Option<&str>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn run_repl(
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
@@ -4474,6 +4265,7 @@ impl LiveCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn handle_session_command(
|
||||
&mut self,
|
||||
action: Option<&str>,
|
||||
@@ -4765,8 +4557,7 @@ fn new_cli_session() -> Result<Session, Box<dyn std::error::Error>> {
|
||||
fn create_managed_session_handle(
|
||||
session_id: &str,
|
||||
) -> Result<SessionHandle, Box<dyn std::error::Error>> {
|
||||
let handle = current_session_store()?
|
||||
.create_handle(session_id);
|
||||
let handle = current_session_store()?.create_handle(session_id);
|
||||
Ok(SessionHandle {
|
||||
id: handle.id,
|
||||
path: handle.path,
|
||||
@@ -5366,14 +5157,14 @@ fn render_config_json(
|
||||
ConfigSource::Project => "project",
|
||||
ConfigSource::Local => "local",
|
||||
};
|
||||
let loaded = runtime_config
|
||||
let is_loaded = runtime_config
|
||||
.loaded_entries()
|
||||
.iter()
|
||||
.any(|le| le.path == e.path);
|
||||
serde_json::json!({
|
||||
"path": e.path.display().to_string(),
|
||||
"source": source,
|
||||
"loaded": loaded,
|
||||
"loaded": is_loaded,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -5798,6 +5589,11 @@ fn format_history_timestamp(timestamp_ms: u64) -> String {
|
||||
|
||||
// Computes civil (Gregorian) year/month/day from days since the Unix epoch
|
||||
// (1970-01-01) using Howard Hinnant's `civil_from_days` algorithm.
|
||||
#[allow(
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_wrap,
|
||||
clippy::cast_possible_truncation
|
||||
)]
|
||||
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||
let z = days + 719_468;
|
||||
let era = if z >= 0 {
|
||||
@@ -6852,29 +6648,11 @@ impl AnthropicRuntimeClient {
|
||||
}
|
||||
|
||||
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
|
||||
let cwd = env::current_dir()?;
|
||||
Ok(resolve_cli_auth_source_for_cwd(&cwd, default_oauth_config)?)
|
||||
Ok(resolve_cli_auth_source_for_cwd()?)
|
||||
}
|
||||
|
||||
fn resolve_cli_auth_source_for_cwd<F>(
|
||||
cwd: &Path,
|
||||
default_oauth: F,
|
||||
) -> Result<AuthSource, api::ApiError>
|
||||
where
|
||||
F: FnOnce() -> OAuthConfig,
|
||||
{
|
||||
resolve_startup_auth_source(|| {
|
||||
Ok(Some(
|
||||
load_runtime_oauth_config_for(cwd)?.unwrap_or_else(default_oauth),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn load_runtime_oauth_config_for(cwd: &Path) -> Result<Option<OAuthConfig>, api::ApiError> {
|
||||
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())
|
||||
fn resolve_cli_auth_source_for_cwd() -> Result<AuthSource, api::ApiError> {
|
||||
resolve_startup_auth_source(|| Ok(None))
|
||||
}
|
||||
|
||||
impl ApiClient for AnthropicRuntimeClient {
|
||||
@@ -6917,7 +6695,6 @@ impl ApiClient for AnthropicRuntimeClient {
|
||||
{
|
||||
// Stalled after tool completion — nudge the model by
|
||||
// re-sending the same request.
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
@@ -8251,8 +8028,6 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw mcp")?;
|
||||
writeln!(out, " claw skills")?;
|
||||
writeln!(out, " claw system-prompt [--cwd PATH] [--date YYYY-MM-DD]")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw logout")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(
|
||||
out,
|
||||
@@ -8336,7 +8111,6 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw mcp show my-server")?;
|
||||
writeln!(out, " claw /skills")?;
|
||||
writeln!(out, " claw doctor")?;
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(out, " claw export")?;
|
||||
writeln!(out, " claw export conversation.md")?;
|
||||
@@ -8612,36 +8386,6 @@ mod tests {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
fn sample_oauth_config(token_url: String) -> OAuthConfig {
|
||||
OAuthConfig {
|
||||
client_id: "runtime-client".to_string(),
|
||||
authorize_url: "https://console.test/oauth/authorize".to_string(),
|
||||
token_url,
|
||||
callback_port: Some(4545),
|
||||
manual_redirect_url: Some("https://console.test/oauth/callback".to_string()),
|
||||
scopes: vec!["org:create_api_key".to_string(), "user:profile".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_token_server(response_body: &'static str) -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("bind listener");
|
||||
let address = listener.local_addr().expect("local addr");
|
||||
thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept connection");
|
||||
let mut buffer = [0_u8; 4096];
|
||||
let _ = stream.read(&mut buffer).expect("read request");
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{}",
|
||||
response_body.len(),
|
||||
response_body
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("write response");
|
||||
});
|
||||
format!("http://{address}/oauth/token")
|
||||
}
|
||||
|
||||
fn with_current_dir<T>(cwd: &Path, f: impl FnOnce() -> T) -> T {
|
||||
let _guard = cwd_lock()
|
||||
.lock()
|
||||
@@ -8784,25 +8528,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_runtime_oauth_config_for_returns_none_without_project_config() {
|
||||
fn resolve_cli_auth_source_ignores_saved_oauth_credentials() {
|
||||
let _guard = env_lock();
|
||||
let root = temp_dir();
|
||||
std::fs::create_dir_all(&root).expect("workspace should exist");
|
||||
|
||||
let oauth = super::load_runtime_oauth_config_for(&root)
|
||||
.expect("loading config should succeed when files are absent");
|
||||
|
||||
std::fs::remove_dir_all(root).expect("temp workspace should clean up");
|
||||
|
||||
assert_eq!(oauth, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_cli_auth_source_uses_default_oauth_when_runtime_config_is_missing() {
|
||||
let _guard = env_lock();
|
||||
let workspace = temp_dir();
|
||||
let config_home = temp_dir();
|
||||
std::fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
std::fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
|
||||
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||
@@ -8820,17 +8548,8 @@ mod tests {
|
||||
})
|
||||
.expect("save expired oauth credentials");
|
||||
|
||||
let token_url = spawn_token_server(
|
||||
r#"{"access_token":"refreshed-access-token","refresh_token":"refreshed-refresh-token","expires_at":4102444800,"scopes":["org:create_api_key","user:profile"]}"#,
|
||||
);
|
||||
|
||||
let auth =
|
||||
super::resolve_cli_auth_source_for_cwd(&workspace, || sample_oauth_config(token_url))
|
||||
.expect("expired saved oauth should refresh via default config");
|
||||
|
||||
let stored = load_oauth_credentials()
|
||||
.expect("load stored credentials")
|
||||
.expect("stored credentials should exist");
|
||||
let error = super::resolve_cli_auth_source_for_cwd()
|
||||
.expect_err("saved oauth should be ignored without env auth");
|
||||
|
||||
match original_config_home {
|
||||
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||
@@ -8844,15 +8563,9 @@ mod tests {
|
||||
Some(value) => std::env::set_var("ANTHROPIC_AUTH_TOKEN", value),
|
||||
None => std::env::remove_var("ANTHROPIC_AUTH_TOKEN"),
|
||||
}
|
||||
std::fs::remove_dir_all(workspace).expect("temp workspace should clean up");
|
||||
std::fs::remove_dir_all(config_home).expect("temp config home should clean up");
|
||||
|
||||
assert_eq!(auth.bearer_token(), Some("refreshed-access-token"));
|
||||
assert_eq!(stored.access_token, "refreshed-access-token");
|
||||
assert_eq!(
|
||||
stored.refresh_token.as_deref(),
|
||||
Some("refreshed-refresh-token")
|
||||
);
|
||||
assert!(error.to_string().contains("ANTHROPIC_API_KEY"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9229,19 +8942,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_login_and_logout_subcommands() {
|
||||
assert_eq!(
|
||||
parse_args(&["login".to_string()]).expect("login should parse"),
|
||||
CliAction::Login {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["logout".to_string()]).expect("logout should parse"),
|
||||
CliAction::Logout {
|
||||
output_format: CliOutputFormat::Text,
|
||||
}
|
||||
);
|
||||
fn removed_login_and_logout_subcommands_error_helpfully() {
|
||||
let login = parse_args(&["login".to_string()]).expect_err("login should be removed");
|
||||
assert!(login.contains("ANTHROPIC_API_KEY"));
|
||||
let logout = parse_args(&["logout".to_string()]).expect_err("logout should be removed");
|
||||
assert!(logout.contains("ANTHROPIC_AUTH_TOKEN"));
|
||||
assert_eq!(
|
||||
parse_args(&["doctor".to_string()]).expect("doctor should parse"),
|
||||
CliAction::Doctor {
|
||||
@@ -10218,6 +9923,8 @@ mod tests {
|
||||
assert!(help.contains("claw mcp"));
|
||||
assert!(help.contains("claw skills"));
|
||||
assert!(help.contains("claw /skills"));
|
||||
assert!(!help.contains("claw login"));
|
||||
assert!(!help.contains("claw logout"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -11308,31 +11015,6 @@ UU conflicted.rs",
|
||||
assert!(!rendered.contains("step 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_browser_failure_keeps_json_stdout_clean() {
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let error = std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"no supported browser opener command found",
|
||||
);
|
||||
|
||||
super::emit_login_browser_open_failure(
|
||||
CliOutputFormat::Json,
|
||||
"https://example.test/oauth/authorize",
|
||||
&error,
|
||||
&mut stdout,
|
||||
&mut stderr,
|
||||
)
|
||||
.expect("browser warning should render");
|
||||
|
||||
assert!(stdout.is_empty());
|
||||
let stderr = String::from_utf8(stderr).expect("utf8");
|
||||
assert!(stderr.contains("failed to open browser automatically"));
|
||||
assert!(stderr.contains("Open this URL manually:"));
|
||||
assert!(stderr.contains("https://example.test/oauth/authorize"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_runtime_plugin_state_merges_plugin_hooks_into_runtime_features() {
|
||||
let config_home = temp_dir();
|
||||
@@ -11620,8 +11302,7 @@ UU conflicted.rs",
|
||||
]);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"--reasoning-effort {value} should be accepted, got: {:?}",
|
||||
result
|
||||
"--reasoning-effort {value} should be accepted, got: {result:?}"
|
||||
);
|
||||
if let Ok(CliAction::Prompt {
|
||||
reasoning_effort, ..
|
||||
|
||||
@@ -639,10 +639,16 @@ fn apply_code_block_background(line: &str) -> String {
|
||||
/// fence markers of equal or greater length are wrapped with a longer fence.
|
||||
///
|
||||
/// LLMs frequently emit triple-backtick code blocks that contain triple-backtick
|
||||
/// examples. CommonMark (and pulldown-cmark) treats the inner marker as the
|
||||
/// examples. `CommonMark` (and pulldown-cmark) treats the inner marker as the
|
||||
/// closing fence, breaking the render. This function detects the situation and
|
||||
/// upgrades the outer fence to use enough backticks (or tildes) that the inner
|
||||
/// markers become ordinary content.
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::items_after_statements,
|
||||
clippy::manual_repeat_n,
|
||||
clippy::manual_str_repeat
|
||||
)]
|
||||
fn normalize_nested_fences(markdown: &str) -> String {
|
||||
// A fence line is either "labeled" (has an info string ⇒ always an opener)
|
||||
// or "bare" (no info string ⇒ could be opener or closer).
|
||||
|
||||
Reference in New Issue
Block a user