mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
## What changed - Add `--chatgpt-token-stdin` for remote HTTPS registration and optional `--chatgpt-account-id`, which must match the token's account claim when present. Read a bounded, newline-terminated token from a pipe without persisting it to `auth.json`. - Load managed configuration using the supplied account, enforce account restrictions, and preserve configured policy-server authentication. Skip ChatGPT policy lookup only for known plans that do not require managed configuration. - Allow `--exit-on-stdin-close` to cancel pending policy loading, and redact the registration bearer from registry diagnostics. - Reject incompatible stdin credential options, including flags placed on opposite sides of `forward`, and disallow ChatGPT token stdin authentication with `forward`. ## Testing Add coverage for account-claim validation, policy selection, policy-server sign-in requirements, shutdown during policy loading, credential flag conflicts, and bearer redaction. GitOrigin-RevId: b6b50565e1f150b96edf3cee796909a991f1b47f
277 lines
10 KiB
Rust
277 lines
10 KiB
Rust
//! Loads executor startup configuration and remote authentication without changing CLI policy.
|
|
|
|
use anyhow::Context;
|
|
use codex_app_server::in_process::EmbeddedNetworkPolicy;
|
|
use codex_cloud_config::chatgpt_config_bundle_loader;
|
|
use codex_cloud_config::managed_config_bundle_loader_for_auth;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_core::config::ConfigBuilder;
|
|
use codex_core::config::ConfigLoadOptions;
|
|
use codex_core::config::bootstrap_auth_config;
|
|
use codex_core::config::find_codex_home;
|
|
use codex_core::config::load_config_toml_with_layer_stack;
|
|
use codex_login::AuthManager;
|
|
use codex_login::CodexAuth;
|
|
use codex_login::read_codex_access_token_from_env;
|
|
use codex_protocol::auth::AuthMode;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use std::io::BufRead;
|
|
use std::io::Read;
|
|
use std::sync::Arc;
|
|
|
|
use crate::exec_server_auth;
|
|
|
|
const REMOTE_AUTH_MODES: &[AuthMode] = &[
|
|
AuthMode::ApiKey,
|
|
AuthMode::Chatgpt,
|
|
AuthMode::ChatgptAuthTokens,
|
|
AuthMode::PersonalAccessToken,
|
|
];
|
|
|
|
pub(super) fn read_chatgpt_stdin_auth(account_id: Option<String>) -> anyhow::Result<CodexAuth> {
|
|
const MAX_FRAME_BYTES: u64 = 64 * 1024;
|
|
let stdin = codex_exec_server::credential_stdin()?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::fd::AsFd;
|
|
use std::os::unix::fs::FileTypeExt;
|
|
let kind = std::fs::File::from(stdin.as_fd().try_clone_to_owned()?)
|
|
.metadata()?
|
|
.file_type();
|
|
anyhow::ensure!(
|
|
kind.is_fifo() || kind.is_socket(),
|
|
"ChatGPT token stdin must be a pipe or socket"
|
|
);
|
|
}
|
|
#[cfg(not(unix))]
|
|
anyhow::ensure!(
|
|
!std::io::IsTerminal::is_terminal(&stdin),
|
|
"ChatGPT token stdin must be piped"
|
|
);
|
|
|
|
// The launcher keeps the pipe open for --exit-on-stdin-close after this line.
|
|
let mut line = String::new();
|
|
stdin.lock().take(MAX_FRAME_BYTES).read_line(&mut line)?;
|
|
chatgpt_auth_from_line(&line, account_id)
|
|
}
|
|
|
|
fn chatgpt_auth_from_line(line: &str, account_id: Option<String>) -> anyhow::Result<CodexAuth> {
|
|
let token = line
|
|
.strip_suffix('\n')
|
|
.context("expected a bounded, newline-terminated ChatGPT access token")?;
|
|
let token = token.strip_suffix('\r').unwrap_or(token);
|
|
anyhow::ensure!(
|
|
!token.is_empty() && token.bytes().all(|b| b.is_ascii_graphic()),
|
|
"invalid ChatGPT access token"
|
|
);
|
|
let claimed_id = codex_login::token_data::parse_chatgpt_jwt_claims(token)
|
|
.map_err(|_| anyhow::anyhow!("invalid ChatGPT access token"))?
|
|
.chatgpt_account_id;
|
|
if let (Some(id), Some(claim)) = (&account_id, &claimed_id) {
|
|
anyhow::ensure!(id == claim, "--chatgpt-account-id does not match the token");
|
|
}
|
|
// A token without an account claim cannot establish the supplied account's plan.
|
|
let plan_override = claimed_id.is_none().then_some("unknown");
|
|
let account_id = account_id
|
|
.or(claimed_id)
|
|
.context("token has no ChatGPT account ID; supply --chatgpt-account-id")?;
|
|
anyhow::ensure!(
|
|
!account_id.is_empty() && account_id.bytes().all(|b| b.is_ascii_graphic()),
|
|
"invalid ChatGPT account ID"
|
|
);
|
|
CodexAuth::from_external_chatgpt_tokens(token, &account_id, plan_override)
|
|
.map_err(|_| anyhow::anyhow!("invalid ChatGPT access token"))
|
|
}
|
|
|
|
pub(super) async fn load_exec_server_remote_auth_provider(
|
|
config: &codex_core::config::Config,
|
|
base_url: &str,
|
|
use_agent_identity_auth: bool,
|
|
) -> anyhow::Result<codex_api::SharedAuthProvider> {
|
|
if use_agent_identity_auth {
|
|
read_codex_access_token_from_env().ok_or_else(|| {
|
|
anyhow::anyhow!("CODEX_ACCESS_TOKEN is required when --use-agent-identity-auth is set")
|
|
})?;
|
|
let auth = AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false)
|
|
.await?
|
|
.auth()
|
|
.await
|
|
.ok_or_else(|| anyhow::anyhow!("Agent Identity authentication is unavailable"))?;
|
|
if !matches!(auth, CodexAuth::AgentIdentity(_)) {
|
|
anyhow::bail!(
|
|
"CODEX_ACCESS_TOKEN did not provide permitted Agent Identity authentication"
|
|
);
|
|
}
|
|
return Ok(codex_model_provider::auth_provider_from_auth(&auth));
|
|
}
|
|
|
|
let (auth_manager, auth) = load_exec_server_remote_auth(
|
|
config,
|
|
"remote exec-server registration requires ChatGPT authentication or API key authentication; run `codex login` or set CODEX_API_KEY",
|
|
)
|
|
.await?;
|
|
|
|
if !is_supported_exec_server_remote_auth(&auth) {
|
|
anyhow::bail!(
|
|
"remote exec-server registration requires ChatGPT authentication or API key authentication; Agent Identity auth requires --use-agent-identity-auth"
|
|
);
|
|
}
|
|
|
|
if auth.is_api_key_auth() {
|
|
validate_api_key_remote_host(base_url)?;
|
|
}
|
|
|
|
if auth_manager.is_workload_identity_selected() {
|
|
Ok(codex_model_provider::auth_provider_from_auth_manager(
|
|
auth_manager,
|
|
&auth,
|
|
))
|
|
} else if auth.is_chatgpt_auth() {
|
|
Ok(exec_server_auth::chatgpt_auth_provider(
|
|
auth_manager,
|
|
&auth,
|
|
)?)
|
|
} else {
|
|
Ok(codex_model_provider::auth_provider_from_auth(&auth))
|
|
}
|
|
}
|
|
|
|
pub(super) fn is_supported_exec_server_remote_auth(auth: &CodexAuth) -> bool {
|
|
auth.is_chatgpt_auth() || auth.is_api_key_auth()
|
|
}
|
|
|
|
pub(super) fn validate_api_key_remote_host(base_url: &str) -> anyhow::Result<()> {
|
|
let url = url::Url::parse(base_url)
|
|
.map_err(|err| anyhow::anyhow!("invalid remote exec-server registration URL: {err}"))?;
|
|
let host = url.host().ok_or_else(|| {
|
|
anyhow::anyhow!("remote exec-server registration URL must include a host")
|
|
})?;
|
|
|
|
let is_loopback = match &host {
|
|
url::Host::Domain(host) => host.eq_ignore_ascii_case("localhost"),
|
|
url::Host::Ipv4(ip) => ip.is_loopback(),
|
|
url::Host::Ipv6(ip) => ip.is_loopback(),
|
|
};
|
|
let is_openai_host = match &host {
|
|
url::Host::Domain(host) => ["openai.com", "openai.org"].into_iter().any(|domain| {
|
|
host.eq_ignore_ascii_case(domain)
|
|
|| host.to_ascii_lowercase().ends_with(&format!(".{domain}"))
|
|
}),
|
|
_ => false,
|
|
};
|
|
let is_allowed = match url.scheme() {
|
|
"https" => is_loopback || is_openai_host,
|
|
"http" => is_loopback,
|
|
_ => false,
|
|
};
|
|
|
|
if !is_allowed {
|
|
anyhow::bail!(
|
|
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn load_exec_server_config(
|
|
root_config_overrides: &CliConfigOverrides,
|
|
strict_config: bool,
|
|
enable_cloud_config: bool,
|
|
stdin_auth: Option<&CodexAuth>,
|
|
) -> anyhow::Result<codex_core::config::Config> {
|
|
let cli_kv_overrides = root_config_overrides
|
|
.parse_overrides()
|
|
.map_err(anyhow::Error::msg)?;
|
|
let bootstrap_cli_overrides = cli_kv_overrides.clone();
|
|
let mut builder = ConfigBuilder::default()
|
|
.cli_overrides(cli_kv_overrides)
|
|
.strict_config(strict_config);
|
|
if enable_cloud_config {
|
|
let codex_home = find_codex_home()?;
|
|
let loader_overrides = LoaderOverrides::default();
|
|
let network_policy = EmbeddedNetworkPolicy::load(&loader_overrides).await;
|
|
let bootstrap_cwd = AbsolutePathBuf::current_dir()?;
|
|
let bootstrap_config = load_config_toml_with_layer_stack(
|
|
&codex_home,
|
|
Some(&bootstrap_cwd),
|
|
bootstrap_cli_overrides,
|
|
ConfigLoadOptions {
|
|
loader_overrides,
|
|
strict_config,
|
|
cloud_config_bundle: Default::default(),
|
|
},
|
|
)
|
|
.await?;
|
|
let bootstrap_auth_config = network_policy
|
|
.bind_bootstrap_auth(bootstrap_auth_config(&codex_home, &bootstrap_config)?);
|
|
let cloud_config_bundle = if let Some(auth) = stdin_auth {
|
|
anyhow::ensure!(
|
|
bootstrap_auth_config.allows_auth(auth),
|
|
"ChatGPT authentication is not permitted for this account"
|
|
);
|
|
managed_config_bundle_loader_for_auth(
|
|
bootstrap_auth_config,
|
|
auth,
|
|
bootstrap_config.config_layer_stack.requirements_toml(),
|
|
)
|
|
.await?
|
|
} else {
|
|
let auth_manager = AuthManager::shared_from_auth_config_with_auth_modes(
|
|
bootstrap_auth_config.clone(),
|
|
/*enable_codex_api_key_env*/ true,
|
|
REMOTE_AUTH_MODES.to_vec(),
|
|
)
|
|
.await?;
|
|
chatgpt_config_bundle_loader(
|
|
auth_manager,
|
|
bootstrap_auth_config
|
|
.chatgpt_base_url
|
|
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
|
bootstrap_auth_config.codex_home,
|
|
bootstrap_auth_config
|
|
.auth_route_config
|
|
.http_client_factory()
|
|
.clone(),
|
|
)
|
|
};
|
|
builder = builder.cloud_config_bundle(cloud_config_bundle);
|
|
}
|
|
let mut config = builder.build().await?;
|
|
if stdin_auth.is_some() {
|
|
config.config_layer_stack.ensure_config_server_applied()?;
|
|
}
|
|
EmbeddedNetworkPolicy::default().activate(&mut config);
|
|
Ok(config)
|
|
}
|
|
|
|
async fn load_exec_server_remote_auth(
|
|
config: &codex_core::config::Config,
|
|
missing_auth_error: &'static str,
|
|
) -> anyhow::Result<(Arc<AuthManager>, codex_login::CodexAuth)> {
|
|
let auth_manager = AuthManager::shared_from_config_with_auth_modes(
|
|
config,
|
|
/*enable_codex_api_key_env*/ true,
|
|
REMOTE_AUTH_MODES.to_vec(),
|
|
)
|
|
.await?;
|
|
|
|
let auth = match auth_manager.auth().await {
|
|
Some(auth) => auth,
|
|
None => {
|
|
auth_manager.reload().await;
|
|
auth_manager
|
|
.auth()
|
|
.await
|
|
.ok_or_else(|| anyhow::anyhow!(missing_auth_error))?
|
|
}
|
|
};
|
|
|
|
Ok((auth_manager, auth))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "config_tests.rs"]
|
|
mod tests;
|