Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 53 additions & 53 deletions codex-rs/Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.0.0"
version = "0.87.0-tryg.1"
# Track the edition for all workspace crates in one place. Individual
# crates can still override this value, but keeping it here means new
# crates created with `cargo new -w ...` automatically inherit the 2024
Expand All @@ -71,7 +71,7 @@ codex-arg0 = { path = "arg0" }
codex-async-utils = { path = "async-utils" }
codex-backend-client = { path = "backend-client" }
codex-chatgpt = { path = "chatgpt" }
codex-cli = { path = "cli"}
codex-cli = { path = "cli" }
codex-client = { path = "codex-client" }
codex-common = { path = "common" }
codex-core = { path = "core" }
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,7 @@ impl CodexMessageProcessor {
scopes.as_deref().unwrap_or_default(),
timeout_secs,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url_template.as_deref(),
)
.await
{
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/cli/src/mcp_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
env_http_headers.clone(),
&Vec::new(),
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url_template.as_deref(),
)
.await?;
println!("Successfully logged in.");
Expand Down Expand Up @@ -355,6 +356,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
env_http_headers,
&scopes,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url_template.as_deref(),
)
.await?;
println!("Successfully logged in to MCP server '{name}'.");
Expand Down
16 changes: 10 additions & 6 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"apply_patch_freeform": {
"type": "boolean"
},
"child_agents_md": {
"type": "boolean"
},
"collab": {
"type": "boolean"
},
Expand All @@ -99,9 +102,6 @@
"experimental_windows_sandbox": {
"type": "boolean"
},
"child_agents_md": {
"type": "boolean"
},
"include_apply_patch_tool": {
"type": "boolean"
},
Expand Down Expand Up @@ -206,6 +206,10 @@
"format": "uint16",
"minimum": 0.0
},
"mcp_oauth_callback_url_template": {
"description": "Optional template for the MCP OAuth `redirect_uri`.\n\nBy default Codex uses a `http://127.0.0.1:{port}/callback` style URL that only works when Codex and the browser run on the same machine. When this field is set, `{port}` will be replaced with the actual callback port and the resulting URL will be sent as `redirect_uri` to the MCP server, allowing non-localhost callbacks on remotes, when used in conjunction with port forwarding.",
"type": "string"
},
"mcp_oauth_credentials_store": {
"description": "Preferred backend for storing MCP OAuth credentials. keyring: Use an OS-specific keyring service. https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs#L2 file: Use a file in the Codex home directory. auto (default): Use the OS-specific keyring service if available, otherwise use a file.",
"default": null,
Expand Down Expand Up @@ -543,6 +547,9 @@
"apply_patch_freeform": {
"type": "boolean"
},
"child_agents_md": {
"type": "boolean"
},
"collab": {
"type": "boolean"
},
Expand All @@ -567,9 +574,6 @@
"experimental_windows_sandbox": {
"type": "boolean"
},
"child_agents_md": {
"type": "boolean"
},
"include_apply_patch_tool": {
"type": "boolean"
},
Expand Down
59 changes: 58 additions & 1 deletion codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ pub struct Config {
/// When unset, Codex will bind to an ephemeral port chosen by the OS.
pub mcp_oauth_callback_port: Option<u16>,

/// Optional template for the MCP OAuth `redirect_uri`.
///
/// By default Codex uses a `http://127.0.0.1:{port}/callback` style URL that only works
/// when Codex and the browser run on the same machine. When this field is set, `{port}`
/// will be replaced with the actual callback port and the resulting URL will be sent as
/// `redirect_uri` to the MCP server, allowing non-localhost callbacks on remotes, when
/// used in conjunction with port forwarding.
pub mcp_oauth_callback_url_template: Option<String>,

/// Combined provider map (defaults merged with user-defined overrides).
pub model_providers: HashMap<String, ModelProviderInfo>,

Expand Down Expand Up @@ -848,11 +857,19 @@ pub struct ConfigToml {
/// auto (default): Use the OS-specific keyring service if available, otherwise use a file.
#[serde(default)]
pub mcp_oauth_credentials_store: Option<OAuthCredentialsStoreMode>,

/// Optional fixed port for the local HTTP callback server used during MCP OAuth login.
/// When unset, Codex will bind to an ephemeral port chosen by the OS.
pub mcp_oauth_callback_port: Option<u16>,

/// Optional template for the MCP OAuth `redirect_uri`.
///
/// By default Codex uses a `http://127.0.0.1:{port}/callback` style URL that only works
/// when Codex and the browser run on the same machine. When this field is set, `{port}`
/// will be replaced with the actual callback port and the resulting URL will be sent as
/// `redirect_uri` to the MCP server, allowing non-localhost callbacks on remotes, when
/// used in conjunction with port forwarding.
pub mcp_oauth_callback_url_template: Option<String>,

/// User-defined provider entries that extend/override the built-in list.
#[serde(default)]
pub model_providers: HashMap<String, ModelProviderInfo>,
Expand Down Expand Up @@ -1491,6 +1508,7 @@ impl Config {
// is important in code to differentiate the mode from the store implementation.
mcp_oauth_credentials_store_mode: cfg.mcp_oauth_credentials_store.unwrap_or_default(),
mcp_oauth_callback_port: cfg.mcp_oauth_callback_port,
mcp_oauth_callback_url_template: cfg.mcp_oauth_callback_url_template,
model_providers,
project_doc_max_bytes: cfg.project_doc_max_bytes.unwrap_or(PROJECT_DOC_MAX_BYTES),
project_doc_fallback_filenames: cfg
Expand Down Expand Up @@ -3597,6 +3615,7 @@ model_verbosity = "high"
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
mcp_oauth_callback_url_template: None,
model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
project_doc_fallback_filenames: Vec::new(),
Expand Down Expand Up @@ -3684,6 +3703,7 @@ model_verbosity = "high"
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
mcp_oauth_callback_url_template: None,
model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
project_doc_fallback_filenames: Vec::new(),
Expand Down Expand Up @@ -3786,6 +3806,7 @@ model_verbosity = "high"
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
mcp_oauth_callback_url_template: None,
model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
project_doc_fallback_filenames: Vec::new(),
Expand Down Expand Up @@ -3874,6 +3895,7 @@ model_verbosity = "high"
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
mcp_oauth_callback_url_template: None,
model_providers: fixture.model_provider_map.clone(),
project_doc_max_bytes: PROJECT_DOC_MAX_BYTES,
project_doc_fallback_filenames: Vec::new(),
Expand Down Expand Up @@ -4215,6 +4237,41 @@ mcp_oauth_callback_port = 5678
Ok(())
}

#[test]
fn config_toml_deserializes_mcp_oauth_callback_url_template() {
let toml =
r#"mcp_oauth_callback_url_template = "https://example.com/proxy/{port}/callback""#;
let cfg: ConfigToml = toml::from_str(toml)
.expect("TOML deserialization should succeed for callback URL template");
assert_eq!(
cfg.mcp_oauth_callback_url_template,
Some("https://example.com/proxy/{port}/callback".to_string())
);
}

#[test]
fn config_loads_mcp_oauth_callback_url_template_from_toml() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let toml = r#"
model = "gpt-5.1"
mcp_oauth_callback_url_template = "https://example.com/proxy/{port}/callback"
"#;
let cfg: ConfigToml = toml::from_str(toml)
.expect("TOML deserialization should succeed for callback URL template");

let config = Config::load_from_base_config_with_overrides(
cfg,
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)?;

assert_eq!(
config.mcp_oauth_callback_url_template,
Some("https://example.com/proxy/{port}/callback".to_string())
);
Ok(())
}

#[test]
fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
18 changes: 15 additions & 3 deletions codex-rs/rmcp-client/src/perform_oauth_login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl Drop for CallbackServerGuard {
}
}

#[allow(clippy::too_many_arguments)]
pub async fn perform_oauth_login(
server_name: &str,
server_url: &str,
Expand All @@ -46,6 +47,7 @@ pub async fn perform_oauth_login(
env_http_headers: Option<HashMap<String, String>>,
scopes: &[String],
callback_port: Option<u16>,
callback_url_template: Option<&str>,
) -> Result<()> {
let headers = OauthHeaders {
http_headers,
Expand All @@ -59,6 +61,7 @@ pub async fn perform_oauth_login(
scopes,
true,
callback_port,
callback_url_template,
None,
)
.await?
Expand All @@ -76,6 +79,7 @@ pub async fn perform_oauth_login_return_url(
scopes: &[String],
timeout_secs: Option<i64>,
callback_port: Option<u16>,
callback_url_template: Option<&str>,
) -> Result<OauthLoginHandle> {
let headers = OauthHeaders {
http_headers,
Expand All @@ -89,6 +93,7 @@ pub async fn perform_oauth_login_return_url(
scopes,
false,
callback_port,
callback_url_template,
timeout_secs,
)
.await?;
Expand Down Expand Up @@ -217,6 +222,7 @@ impl OauthLoginFlow {
scopes: &[String],
launch_browser: bool,
callback_port: Option<u16>,
callback_url_template: Option<&str>,
timeout_secs: Option<i64>,
) -> Result<Self> {
const DEFAULT_OAUTH_TIMEOUT_SECS: i64 = 300;
Expand All @@ -232,21 +238,27 @@ impl OauthLoginFlow {
server: Arc::clone(&server),
};

let redirect_uri = match server.server_addr() {
let (callback_port, default_callback_url) = match server.server_addr() {
tiny_http::ListenAddr::IP(std::net::SocketAddr::V4(addr)) => {
let ip = addr.ip();
let port = addr.port();
format!("http://{ip}:{port}/callback")
(port, format!("http://{ip}:{port}/callback"))
}
tiny_http::ListenAddr::IP(std::net::SocketAddr::V6(addr)) => {
let ip = addr.ip();
let port = addr.port();
format!("http://[{ip}]:{port}/callback")
(port, format!("http://[{ip}]:{port}/callback"))
}
#[cfg(not(target_os = "windows"))]
_ => return Err(anyhow!("unable to determine callback address")),
};

let redirect_uri = if let Some(template) = callback_url_template {
template.replace("{port}", &callback_port.to_string())
} else {
default_callback_url
};

let (tx, rx) = oneshot::channel();
spawn_callback_server(server, tx);

Expand Down
20 changes: 20 additions & 0 deletions scripts/build_codex_cli_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

set -euo pipefail

# Usage: ./build_codex_cli_image.sh <version>
# If no version is provided, default to "latest"
VERSION="${1:-latest}"

echo "Building Codex CLI Docker image with version: $VERSION"

cd "codex-rs"
cargo build --target x86_64-unknown-linux-gnu --release --bin codex

docker build -t "capcr.azurecr.io/codex/codex-cli:$VERSION" -f- . <<'EOF'
FROM scratch
COPY target/x86_64-unknown-linux-gnu/release/codex /codex
ENTRYPOINT ["/codex"]
EOF

docker push capcr.azurecr.io/codex/codex-cli:"$VERSION"
Loading