mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Allow API-key auth for remote exec-server registration (#24666)
## Overview Allow remote `codex exec-server` registration to use existing API-key auth while restricting where those credentials can be sent. - Accept `CodexAuth::ApiKey` for the normal `--remote` registration path. - Restrict API-key remote registration to HTTPS `openai.com` and `openai.org` hosts and subdomains, with explicit HTTP loopback support for local development. - Disable registry registration redirects so credentials cannot be forwarded to an unvalidated destination. - Retain `--use-agent-identity-auth` as the explicit Agent Identity path. - Document remote registration using `CODEX_API_KEY`. ## Big picture Callers can now provide an API key directly to `exec-server` registration without first establishing ChatGPT login state: ```sh CODEX_API_KEY="$OPENAI_API_KEY" \ codex exec-server \ --remote "https://<host>.openai.org/api" \ --environment-id "$ENVIRONMENT_ID" ``` ## Validation - `cargo fmt --all` (`just fmt` is not installed on this host) - `cargo test -p codex-cli -p codex-exec-server`
This commit is contained in:
committed by
GitHub
Unverified
parent
26c9502121
commit
c57dee98b7
+105
-4
@@ -1486,7 +1486,8 @@ async fn run_exec_server_command(
|
||||
.ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?;
|
||||
let config = load_exec_server_config(root_config_overrides, strict_config).await?;
|
||||
let auth_provider =
|
||||
load_exec_server_remote_auth_provider(&config, cmd.use_agent_identity_auth).await?;
|
||||
load_exec_server_remote_auth_provider(&config, &base_url, cmd.use_agent_identity_auth)
|
||||
.await?;
|
||||
let mut remote_config = codex_exec_server::RemoteEnvironmentConfig::new(
|
||||
base_url,
|
||||
environment_id,
|
||||
@@ -1516,6 +1517,7 @@ async fn run_exec_server_command(
|
||||
|
||||
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 {
|
||||
@@ -1530,19 +1532,61 @@ async fn load_exec_server_remote_auth_provider(
|
||||
|
||||
let auth = load_exec_server_remote_auth(
|
||||
config,
|
||||
"remote exec-server registration requires ChatGPT authentication; run `codex login` first",
|
||||
"remote exec-server registration requires ChatGPT authentication or API key authentication; run `codex login` or set CODEX_API_KEY",
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !auth.is_chatgpt_auth() {
|
||||
if !is_supported_exec_server_remote_auth(&auth) {
|
||||
anyhow::bail!(
|
||||
"remote exec-server registration requires ChatGPT authentication; API key and Agent Identity auth are not supported"
|
||||
"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)?;
|
||||
}
|
||||
|
||||
Ok(codex_model_provider::auth_provider_from_auth(&auth))
|
||||
}
|
||||
|
||||
fn is_supported_exec_server_remote_auth(auth: &CodexAuth) -> bool {
|
||||
auth.is_chatgpt_auth() || auth.is_api_key_auth()
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn load_exec_server_config(
|
||||
root_config_overrides: &CliConfigOverrides,
|
||||
strict_config: bool,
|
||||
@@ -2168,6 +2212,63 @@ mod tests {
|
||||
use codex_tui::TokenUsage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_auth_accepts_api_key_auth() {
|
||||
let auth = CodexAuth::from_api_key("sk-test");
|
||||
|
||||
assert!(is_supported_exec_server_remote_auth(&auth));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_api_key_auth_accepts_https_openai_domains() {
|
||||
for base_url in [
|
||||
"https://openai.com/api",
|
||||
"https://service.openai.com/api",
|
||||
"https://openai.org/api",
|
||||
"https://service.openai.org/api",
|
||||
] {
|
||||
assert!(validate_api_key_remote_host(base_url).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_api_key_auth_accepts_http_loopback() {
|
||||
for base_url in [
|
||||
"http://localhost:8098/api",
|
||||
"http://127.0.0.1:8098/api",
|
||||
"http://[::1]:8098/api",
|
||||
] {
|
||||
assert!(validate_api_key_remote_host(base_url).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_api_key_auth_rejects_http_openai_domain() {
|
||||
for base_url in [
|
||||
"http://service.openai.com/api",
|
||||
"http://service.openai.org/api",
|
||||
] {
|
||||
let error = validate_api_key_remote_host(base_url)
|
||||
.expect_err("reject plaintext OpenAI destination");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_api_key_auth_rejects_suffix_spoof() {
|
||||
let error = validate_api_key_remote_host("https://service.openai.org.evil.example/api")
|
||||
.expect_err("reject suffix spoof");
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"remote exec-server API-key authentication is restricted to HTTPS openai.com and openai.org hosts and subdomains or loopback hosts"
|
||||
);
|
||||
}
|
||||
|
||||
fn finalize_resume_from_args(args: &[&str]) -> TuiCli {
|
||||
let cli = MultitoolCli::try_parse_from(args).expect("parse");
|
||||
let MultitoolCli {
|
||||
|
||||
Reference in New Issue
Block a user