Skip to content
Draft
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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions codex-rs/backend-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::types::CodeTaskDetailsResponse;
use crate::types::ConfigFileResponse;
use crate::types::ListCredentialRoutesResponse;
use crate::types::PaginatedListTaskListItem;
use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
use crate::types::RateLimitStatusPayload;
Expand Down Expand Up @@ -408,6 +409,20 @@ impl Client {
.map_err(RequestError::from)
}

pub async fn list_credential_routes(&self) -> Result<ListCredentialRoutesResponse> {
let url = self.credential_routes_url();
let req = self.http.get(&url).headers(self.headers());
let (body, ct) = self.exec_request(req, "GET", &url).await?;
self.decode_json(&url, &ct, &body)
}

pub fn credential_routes_proxy_url(&self) -> String {
match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/credential_routes/proxy", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/credential_routes/proxy", self.base_url),
}
}

/// Create a new task (user turn) by POSTing to the appropriate backend path
/// based on `path_style`. Returns the created task id.
pub async fn create_task(&self, request_body: serde_json::Value) -> Result<String> {
Expand Down Expand Up @@ -539,6 +554,13 @@ impl Client {
}
}

fn credential_routes_url(&self) -> String {
match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/credential_routes", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/credential_routes", self.base_url),
}
}

fn map_rate_limit_window(
window: Option<Option<Box<crate::types::RateLimitWindowSnapshot>>>,
) -> Option<RateLimitWindow> {
Expand Down Expand Up @@ -862,4 +884,43 @@ mod tests {
serde_json::json!({ "credit_type": "usage_limit" })
);
}

#[test]
fn credential_routes_use_expected_paths() {
let codex_client = Client {
base_url: "https://example.test".to_string(),
http: reqwest::Client::new(),
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
user_agent: None,
chatgpt_account_id: None,
chatgpt_account_is_fedramp: false,
path_style: PathStyle::CodexApi,
};
assert_eq!(
codex_client.credential_routes_url(),
"https://example.test/api/codex/credential_routes"
);
assert_eq!(
codex_client.credential_routes_proxy_url(),
"https://example.test/api/codex/credential_routes/proxy"
);

let chatgpt_client = Client {
base_url: "https://chatgpt.com/backend-api".to_string(),
http: reqwest::Client::new(),
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
user_agent: None,
chatgpt_account_id: None,
chatgpt_account_is_fedramp: false,
path_style: PathStyle::ChatGptApi,
};
assert_eq!(
chatgpt_client.credential_routes_url(),
"https://chatgpt.com/backend-api/wham/credential_routes"
);
assert_eq!(
chatgpt_client.credential_routes_proxy_url(),
"https://chatgpt.com/backend-api/wham/credential_routes/proxy"
);
}
}
3 changes: 3 additions & 0 deletions codex-rs/backend-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ pub use client::RequestError;
pub use types::CodeTaskDetailsResponse;
pub use types::CodeTaskDetailsResponseExt;
pub use types::ConfigFileResponse;
pub use types::CredentialRouteAuthType;
pub use types::ListCredentialRoutesResponse;
pub use types::PaginatedListTaskListItem;
pub use types::ResolvedCredentialRoute;
pub use types::TaskListItem;
pub use types::TurnAttemptsSiblingTurnsResponse;
23 changes: 23 additions & 0 deletions codex-rs/backend-client/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@ use serde::de::Deserializer;
use serde_json::Value;
use std::collections::HashMap;

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ListCredentialRoutesResponse {
pub routes: Vec<ResolvedCredentialRoute>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct ResolvedCredentialRoute {
pub connector_id: String,
pub link_id: String,
pub auth_type: CredentialRouteAuthType,
pub base_url: String,
}

#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
pub enum CredentialRouteAuthType {
#[serde(rename = "API_KEY")]
ApiKey,
#[serde(rename = "OAUTH")]
OAuth,
#[serde(rename = "NONE")]
None,
}

/// Hand-rolled models for the Cloud Tasks task-details response.
/// The generated OpenAPI models are pretty bad. This is a half-step
/// towards hand-rolling them.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ codex-api = { workspace = true }
codex-app-server-protocol = { workspace = true }
codex-apply-patch = { workspace = true }
codex-async-utils = { workspace = true }
codex-backend-client = { workspace = true }
codex-code-mode = { workspace = true }
codex-connectors = { workspace = true }
codex-config = { workspace = true }
Expand Down
36 changes: 36 additions & 0 deletions codex-rs/core/src/credentialed_routes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use codex_backend_client::Client as BackendClient;
use codex_backend_client::ResolvedCredentialRoute;
use codex_login::CodexAuth;
use tracing::debug;
use tracing::warn;

pub(crate) async fn load_for_session(
chatgpt_base_url: &str,
auth: Option<&CodexAuth>,
) -> Vec<ResolvedCredentialRoute> {
let Some(auth) = auth.filter(|auth| auth.uses_codex_backend()) else {
return Vec::new();
};

let client = match BackendClient::from_auth(chatgpt_base_url.to_string(), auth) {
Ok(client) => client,
Err(err) => {
warn!(error = %err, "failed to initialize credentialed routes client");
return Vec::new();
}
};

match client.list_credential_routes().await {
Ok(response) => {
debug!(
credentialed_routes = response.routes.len(),
"loaded credentialed routes for session"
);
response.routes
}
Err(err) => {
warn!(error = %err, "failed to load credentialed routes for session");
Vec::new()
}
}
}
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod codex_thread;
mod compact_remote;
mod compact_remote_v2;
mod config_lock;
mod credentialed_routes;
pub use codex_thread::CodexThread;
pub use codex_thread::CodexThreadTurnContextOverrides;
pub use codex_thread::ThreadConfigSnapshot;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,13 +930,18 @@ impl Session {

async fn start_managed_network_proxy(
spec: &crate::config::NetworkProxySpec,
credentialed_routes: &[codex_backend_client::ResolvedCredentialRoute],
exec_policy: &codex_execpolicy::Policy,
permission_profile: &PermissionProfile,
network_policy_decider: Option<Arc<dyn codex_network_proxy::NetworkPolicyDecider>>,
blocked_request_observer: Option<Arc<dyn codex_network_proxy::BlockedRequestObserver>>,
managed_network_requirements_enabled: bool,
audit_metadata: NetworkProxyAuditMetadata,
) -> anyhow::Result<(StartedNetworkProxy, SessionNetworkProxyRuntime)> {
debug!(
credentialed_routes = credentialed_routes.len(),
"starting managed network proxy"
);
let spec = spec
.with_exec_policy_network_rules(exec_policy)
.map_err(|err| {
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/session/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,9 +765,13 @@ impl Session {
});
let (network_proxy, session_network_proxy) =
if let Some(spec) = config.permissions.network.as_ref() {
let credentialed_routes =
crate::credentialed_routes::load_for_session(&config.chatgpt_base_url, auth)
.await;
let current_exec_policy = exec_policy.current();
let (network_proxy, session_network_proxy) = Self::start_managed_network_proxy(
spec,
&credentialed_routes,
current_exec_policy.as_ref(),
config.permissions.permission_profile.get(),
network_policy_decider.as_ref().map(Arc::clone),
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,7 @@ async fn start_managed_network_proxy_applies_execpolicy_network_rules() -> anyho

let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
/*credentialed_routes*/ &[],
&exec_policy,
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
Expand Down Expand Up @@ -726,6 +727,7 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules()

let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
/*credentialed_routes*/ &[],
&exec_policy,
&permission_profile_for_sandbox_policy(&SandboxPolicy::new_workspace_write_policy()),
/*network_policy_decider*/ None,
Expand Down Expand Up @@ -765,6 +767,7 @@ async fn managed_network_proxy_decider_survives_full_access_start() -> anyhow::R

let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
/*credentialed_routes*/ &[],
&exec_policy,
&permission_profile_for_sandbox_policy(&SandboxPolicy::DangerFullAccess),
Some(network_policy_decider),
Expand Down Expand Up @@ -837,6 +840,7 @@ async fn new_turn_refreshes_managed_network_proxy_for_sandbox_change() -> anyhow
)?;
let (started_proxy, _) = Session::start_managed_network_proxy(
&spec,
/*credentialed_routes*/ &[],
&Policy::empty(),
&permission_profile_for_sandbox_policy(&initial_policy),
/*network_policy_decider*/ None,
Expand Down
Loading