mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
## Why Once the repo-local lint exists, `codex-rs` needs to follow the checked-in convention and CI needs to keep it from drifting. This commit applies the fallback `/*param*/` style consistently across existing positional literal call sites without changing those APIs. The longer-term preference is still to avoid APIs that require comments by choosing clearer parameter types and call shapes. This PR is intentionally the mechanical follow-through for the places where the existing signatures stay in place. After rebasing onto newer `main`, the rollout also had to cover newly introduced `tui_app_server` call sites. That made it clear the first cut of the CI job was too expensive for the common path: it was spending almost as much time installing `cargo-dylint` and re-testing the lint crate as a representative test job spends running product tests. The CI update keeps the full workspace enforcement but trims that extra overhead from ordinary `codex-rs` PRs. ## What changed - keep a dedicated `argument_comment_lint` job in `rust-ci` - mechanically annotate remaining opaque positional literals across `codex-rs` with exact `/*param*/` comments, including the rebased `tui_app_server` call sites that now fall under the lint - keep the checked-in style aligned with the lint policy by using `/*param*/` and leaving string and char literals uncommented - cache `cargo-dylint`, `dylint-link`, and the relevant Cargo registry/git metadata in the lint job - split changed-path detection so the lint crate's own `cargo test` step runs only when `tools/argument-comment-lint/*` or `rust-ci.yml` changes - continue to run the repo wrapper over the `codex-rs` workspace, so product-code enforcement is unchanged Most of the code changes in this commit are intentionally mechanical comment rewrites or insertions driven by the lint itself. ## Verification - `./tools/argument-comment-lint/run.sh --workspace` - `cargo test -p codex-tui-app-server -p codex-tui` - parsed `.github/workflows/rust-ci.yml` locally with PyYAML --- * -> #14652 * #14651
229 lines
6.9 KiB
Rust
229 lines
6.9 KiB
Rust
use reqwest::StatusCode;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde::de::Deserializer;
|
|
use serde::de::{self};
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
use crate::pkce::PkceCodes;
|
|
use crate::server::ServerOptions;
|
|
use codex_client::build_reqwest_client_with_custom_ca;
|
|
use std::io;
|
|
|
|
const ANSI_BLUE: &str = "\x1b[94m";
|
|
const ANSI_GRAY: &str = "\x1b[90m";
|
|
const ANSI_RESET: &str = "\x1b[0m";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DeviceCode {
|
|
pub verification_url: String,
|
|
pub user_code: String,
|
|
device_auth_id: String,
|
|
interval: u64,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct UserCodeResp {
|
|
device_auth_id: String,
|
|
#[serde(alias = "user_code", alias = "usercode")]
|
|
user_code: String,
|
|
#[serde(default, deserialize_with = "deserialize_interval")]
|
|
interval: u64,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct UserCodeReq {
|
|
client_id: String,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct TokenPollReq {
|
|
device_auth_id: String,
|
|
user_code: String,
|
|
}
|
|
|
|
fn deserialize_interval<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s = String::deserialize(deserializer)?;
|
|
s.trim().parse::<u64>().map_err(de::Error::custom)
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct CodeSuccessResp {
|
|
authorization_code: String,
|
|
code_challenge: String,
|
|
code_verifier: String,
|
|
}
|
|
|
|
/// Request the user code and polling interval.
|
|
async fn request_user_code(
|
|
client: &reqwest::Client,
|
|
auth_base_url: &str,
|
|
client_id: &str,
|
|
) -> std::io::Result<UserCodeResp> {
|
|
let url = format!("{auth_base_url}/deviceauth/usercode");
|
|
let body = serde_json::to_string(&UserCodeReq {
|
|
client_id: client_id.to_string(),
|
|
})
|
|
.map_err(std::io::Error::other)?;
|
|
let resp = client
|
|
.post(url)
|
|
.header("Content-Type", "application/json")
|
|
.body(body)
|
|
.send()
|
|
.await
|
|
.map_err(std::io::Error::other)?;
|
|
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
if status == StatusCode::NOT_FOUND {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::NotFound,
|
|
"device code login is not enabled for this Codex server. Use the browser login or verify the server URL.",
|
|
));
|
|
}
|
|
|
|
return Err(std::io::Error::other(format!(
|
|
"device code request failed with status {status}"
|
|
)));
|
|
}
|
|
|
|
let body = resp.text().await.map_err(std::io::Error::other)?;
|
|
serde_json::from_str(&body).map_err(std::io::Error::other)
|
|
}
|
|
|
|
/// Poll token endpoint until a code is issued or timeout occurs.
|
|
async fn poll_for_token(
|
|
client: &reqwest::Client,
|
|
auth_base_url: &str,
|
|
device_auth_id: &str,
|
|
user_code: &str,
|
|
interval: u64,
|
|
) -> std::io::Result<CodeSuccessResp> {
|
|
let url = format!("{auth_base_url}/deviceauth/token");
|
|
let max_wait = Duration::from_secs(15 * 60);
|
|
let start = Instant::now();
|
|
|
|
loop {
|
|
let body = serde_json::to_string(&TokenPollReq {
|
|
device_auth_id: device_auth_id.to_string(),
|
|
user_code: user_code.to_string(),
|
|
})
|
|
.map_err(std::io::Error::other)?;
|
|
let resp = client
|
|
.post(&url)
|
|
.header("Content-Type", "application/json")
|
|
.body(body)
|
|
.send()
|
|
.await
|
|
.map_err(std::io::Error::other)?;
|
|
|
|
let status = resp.status();
|
|
|
|
if status.is_success() {
|
|
return resp.json().await.map_err(std::io::Error::other);
|
|
}
|
|
|
|
if status == StatusCode::FORBIDDEN || status == StatusCode::NOT_FOUND {
|
|
if start.elapsed() >= max_wait {
|
|
return Err(std::io::Error::other(
|
|
"device auth timed out after 15 minutes",
|
|
));
|
|
}
|
|
let sleep_for = Duration::from_secs(interval).min(max_wait - start.elapsed());
|
|
tokio::time::sleep(sleep_for).await;
|
|
continue;
|
|
}
|
|
|
|
return Err(std::io::Error::other(format!(
|
|
"device auth failed with status {}",
|
|
resp.status()
|
|
)));
|
|
}
|
|
}
|
|
|
|
fn print_device_code_prompt(verification_url: &str, code: &str) {
|
|
let version = env!("CARGO_PKG_VERSION");
|
|
println!(
|
|
"\nWelcome to Codex [v{ANSI_GRAY}{version}{ANSI_RESET}]\n{ANSI_GRAY}OpenAI's command-line coding agent{ANSI_RESET}\n\
|
|
\nFollow these steps to sign in with ChatGPT using device code authorization:\n\
|
|
\n1. Open this link in your browser and sign in to your account\n {ANSI_BLUE}{verification_url}{ANSI_RESET}\n\
|
|
\n2. Enter this one-time code {ANSI_GRAY}(expires in 15 minutes){ANSI_RESET}\n {ANSI_BLUE}{code}{ANSI_RESET}\n\
|
|
\n{ANSI_GRAY}Device codes are a common phishing target. Never share this code.{ANSI_RESET}\n",
|
|
);
|
|
}
|
|
|
|
pub async fn request_device_code(opts: &ServerOptions) -> std::io::Result<DeviceCode> {
|
|
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
|
let base_url = opts.issuer.trim_end_matches('/');
|
|
let api_base_url = format!("{base_url}/api/accounts");
|
|
let uc = request_user_code(&client, &api_base_url, &opts.client_id).await?;
|
|
|
|
Ok(DeviceCode {
|
|
verification_url: format!("{base_url}/codex/device"),
|
|
user_code: uc.user_code,
|
|
device_auth_id: uc.device_auth_id,
|
|
interval: uc.interval,
|
|
})
|
|
}
|
|
|
|
pub async fn complete_device_code_login(
|
|
opts: ServerOptions,
|
|
device_code: DeviceCode,
|
|
) -> std::io::Result<()> {
|
|
let client = build_reqwest_client_with_custom_ca(reqwest::Client::builder())?;
|
|
let base_url = opts.issuer.trim_end_matches('/');
|
|
let api_base_url = format!("{base_url}/api/accounts");
|
|
|
|
let code_resp = poll_for_token(
|
|
&client,
|
|
&api_base_url,
|
|
&device_code.device_auth_id,
|
|
&device_code.user_code,
|
|
device_code.interval,
|
|
)
|
|
.await?;
|
|
|
|
let pkce = PkceCodes {
|
|
code_verifier: code_resp.code_verifier,
|
|
code_challenge: code_resp.code_challenge,
|
|
};
|
|
let redirect_uri = format!("{base_url}/deviceauth/callback");
|
|
|
|
let tokens = crate::server::exchange_code_for_tokens(
|
|
base_url,
|
|
&opts.client_id,
|
|
&redirect_uri,
|
|
&pkce,
|
|
&code_resp.authorization_code,
|
|
)
|
|
.await
|
|
.map_err(|err| std::io::Error::other(format!("device code exchange failed: {err}")))?;
|
|
|
|
if let Err(message) = crate::server::ensure_workspace_allowed(
|
|
opts.forced_chatgpt_workspace_id.as_deref(),
|
|
&tokens.id_token,
|
|
) {
|
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, message));
|
|
}
|
|
|
|
crate::server::persist_tokens_async(
|
|
&opts.codex_home,
|
|
/*api_key*/ None,
|
|
tokens.id_token,
|
|
tokens.access_token,
|
|
tokens.refresh_token,
|
|
opts.cli_auth_credentials_store_mode,
|
|
)
|
|
.await
|
|
}
|
|
|
|
pub async fn run_device_code_login(opts: ServerOptions) -> std::io::Result<()> {
|
|
let device_code = request_device_code(&opts).await?;
|
|
print_device_code_prompt(&device_code.verification_url, &device_code.user_code);
|
|
complete_device_code_login(opts, device_code).await
|
|
}
|