mirror of
https://github.com/openai/codex.git
synced 2026-10-16 16:58:46 +00:00
Keep credentials out of app-server logs (#39993)
## Why App-server logs can be persisted or included in submitted diagnostics, so credentials used by model providers, authentication refreshes, and attestation requests must not appear in diagnostic output. ## What changed - Add `RedactedString`, which preserves serialization and string access while replacing debug output with `<redacted>`. - Use it for model-provider bearer tokens, header and query values, authentication command arguments, and attestation tokens. - Avoid logging JSON-RPC error payloads and parser or authentication errors that may echo credentials; retain safe context such as error codes and categories. ## Testing - Add an app-server regression test that exercises provider credentials, refreshed authentication tokens, and attestation tokens, then verifies none appear in persisted SQLite or submitted diagnostic logs. GitOrigin-RevId: 8c50408adf94d93847658b1320682cf3b637d2cc
This commit is contained in:
Generated
+14
@@ -2145,6 +2145,7 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-path-uri",
|
||||
"codex-utils-redacted-string",
|
||||
"inventory",
|
||||
"pretty_assertions",
|
||||
"rmcp",
|
||||
@@ -2654,6 +2655,7 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path",
|
||||
"codex-utils-path-uri",
|
||||
"codex-utils-redacted-string",
|
||||
"core-foundation 0.9.4",
|
||||
"dns-lookup",
|
||||
"dunce",
|
||||
@@ -2807,6 +2809,7 @@ dependencies = [
|
||||
"codex-utils-path-uri",
|
||||
"codex-utils-plugins",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-redacted-string",
|
||||
"codex-utils-stream-parser",
|
||||
"codex-utils-string",
|
||||
"codex-web-search-extension",
|
||||
@@ -3751,6 +3754,7 @@ dependencies = [
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-response-debug-context",
|
||||
"codex-utils-redacted-string",
|
||||
"http 1.4.0",
|
||||
"pretty_assertions",
|
||||
"serde_json",
|
||||
@@ -3766,6 +3770,7 @@ dependencies = [
|
||||
"codex-api",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-redacted-string",
|
||||
"http 1.4.0",
|
||||
"maplit",
|
||||
"pretty_assertions",
|
||||
@@ -3936,6 +3941,7 @@ dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-image",
|
||||
"codex-utils-path-uri",
|
||||
"codex-utils-redacted-string",
|
||||
"codex-utils-string",
|
||||
"encoding_rs",
|
||||
"globset",
|
||||
@@ -4674,6 +4680,14 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-redacted-string"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-rustls-provider"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -109,6 +109,7 @@ members = [
|
||||
"utils/home-dir",
|
||||
"utils/pty",
|
||||
"utils/readiness",
|
||||
"utils/redacted-string",
|
||||
"utils/rustls-provider",
|
||||
"utils/string",
|
||||
"utils/cli",
|
||||
@@ -269,6 +270,7 @@ codex-utils-path = { path = "utils/path-utils" }
|
||||
codex-utils-path-uri = { path = "utils/path-uri" }
|
||||
codex-utils-plugins = { path = "utils/plugins" }
|
||||
codex-utils-pty = { path = "utils/pty" }
|
||||
codex-utils-redacted-string = { path = "utils/redacted-string" }
|
||||
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
|
||||
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
|
||||
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
|
||||
|
||||
@@ -24,6 +24,7 @@ codex-secrets = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::JsonSchema;
|
||||
use crate::TS;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -13,5 +14,6 @@ pub struct AttestationGenerateParams {}
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct AttestationGenerateResponse {
|
||||
/// Opaque client attestation token.
|
||||
pub token: String,
|
||||
#[ts(type = "string")]
|
||||
pub token: RedactedString,
|
||||
}
|
||||
|
||||
@@ -82,11 +82,8 @@ async fn request_attestation_header_value_with_timeout(
|
||||
let result = match timeout(timeout_duration, rx).await {
|
||||
Ok(Ok(Ok(result))) => result,
|
||||
Ok(Ok(Err(err))) => {
|
||||
warn!(
|
||||
code = err.code,
|
||||
message = %err.message,
|
||||
"attestation generation request failed"
|
||||
);
|
||||
// Don't log err.message because it may contain a token.
|
||||
warn!(code = err.code, "attestation generation request failed");
|
||||
return app_server_attestation_header_value(
|
||||
AppServerAttestationStatus::RequestFailed,
|
||||
/*token*/ None,
|
||||
@@ -118,7 +115,10 @@ async fn request_attestation_header_value_with_timeout(
|
||||
Some(&response.token),
|
||||
),
|
||||
Err(err) => {
|
||||
warn!("failed to deserialize attestation generation response: {err}");
|
||||
warn!(
|
||||
error_category = ?err.classify(),
|
||||
"failed to deserialize attestation generation response"
|
||||
);
|
||||
app_server_attestation_header_value(
|
||||
AppServerAttestationStatus::MalformedResponse,
|
||||
/*token*/ None,
|
||||
|
||||
@@ -49,10 +49,9 @@ impl ExternalAuthBridge {
|
||||
std::io::Error::other(format!("auth refresh request canceled: {err}"))
|
||||
})?;
|
||||
result.map_err(|err| {
|
||||
std::io::Error::other(format!(
|
||||
"auth refresh request failed: code={} message={}",
|
||||
err.code, err.message
|
||||
))
|
||||
// Don't log err.message because it may contain a token.
|
||||
let code = err.code;
|
||||
std::io::Error::other(format!("auth refresh request failed: code={code}"))
|
||||
})?
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -64,13 +63,17 @@ impl ExternalAuthBridge {
|
||||
}
|
||||
};
|
||||
|
||||
let response: ChatgptAuthTokensRefreshResponse =
|
||||
serde_json::from_value(result).map_err(std::io::Error::other)?;
|
||||
// Don't propagate parser error messages because they may contain a token.
|
||||
let response: ChatgptAuthTokensRefreshResponse = serde_json::from_value(result)
|
||||
.map_err(|_| std::io::Error::other("invalid auth refresh response"))?;
|
||||
let auth = CodexAuth::from_external_chatgpt_tokens(
|
||||
response.access_token.as_str(),
|
||||
response.chatgpt_account_id.as_str(),
|
||||
response.chatgpt_plan_type.as_deref(),
|
||||
)?;
|
||||
)
|
||||
.map_err(|err| {
|
||||
std::io::Error::new(err.kind(), "auth refresh returned invalid credentials")
|
||||
})?;
|
||||
*self
|
||||
.auth
|
||||
.write()
|
||||
|
||||
@@ -829,7 +829,6 @@ impl MessageProcessor {
|
||||
|
||||
/// Handle an error object received from the peer.
|
||||
pub(crate) async fn process_error(&self, err: JSONRPCError) {
|
||||
tracing::error!("<- error: {:?}", err);
|
||||
self.outgoing.notify_client_error(err.id, err.error).await;
|
||||
}
|
||||
|
||||
|
||||
@@ -408,11 +408,12 @@ impl OutgoingMessageSender {
|
||||
|
||||
match entry {
|
||||
Some((id, entry)) => {
|
||||
warn!("client responded with error for {id:?}: {error:?}");
|
||||
// Don't log error messages or data because they may contain credentials.
|
||||
warn!(code = error.code, "client responded with error for {id:?}");
|
||||
self.analytics_events_client
|
||||
.track_server_request_aborted(now_unix_timestamp_ms(), id.clone());
|
||||
if let Err(err) = entry.callback.send(Err(error)) {
|
||||
warn!("could not notify callback for {id:?} due to: {err:?}");
|
||||
if entry.callback.send(Err(error)).is_err() {
|
||||
warn!("could not notify callback for {id:?}: receiver dropped");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
@@ -445,10 +446,10 @@ impl OutgoingMessageSender {
|
||||
self.analytics_events_client
|
||||
.track_server_request_aborted(now_unix_timestamp_ms(), entry.request.id().clone());
|
||||
if let Some(error) = error.as_ref()
|
||||
&& let Err(err) = entry.callback.send(Err(error.clone()))
|
||||
&& entry.callback.send(Err(error.clone())).is_err()
|
||||
{
|
||||
let request_id = entry.request.id();
|
||||
warn!("could not notify callback for {request_id:?} due to: {err:?}");
|
||||
warn!("could not notify callback for {request_id:?}: receiver dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,10 +504,10 @@ impl OutgoingMessageSender {
|
||||
self.analytics_events_client
|
||||
.track_server_request_aborted(now_unix_timestamp_ms(), entry.request.id().clone());
|
||||
if let Some(error) = error.as_ref()
|
||||
&& let Err(err) = entry.callback.send(Err(error.clone()))
|
||||
&& entry.callback.send(Err(error.clone())).is_err()
|
||||
{
|
||||
let request_id = entry.request.id();
|
||||
warn!("could not notify callback for {request_id:?} due to: {err:?}",);
|
||||
warn!("could not notify callback for {request_id:?}: receiver dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptIdTokenClaims;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::app_server_json_shutdown_event;
|
||||
use app_test_support::create_exec_command_sse_response;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::encode_id_token;
|
||||
use app_test_support::write_models_cache;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::InitializeCapabilities;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::LoginAccountResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use codex_features::Feature;
|
||||
use codex_state::LogQuery;
|
||||
use codex_state::SqliteConfig;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
@@ -19,9 +33,214 @@ use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
const READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn credentials_stay_out_of_persisted_and_feedback_logs() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let bearer = "synthetic-provider-bearer";
|
||||
let header = "synthetic-provider-header";
|
||||
let attestation = "synthetic-attestation-token";
|
||||
let account_id = "123e4567-e89b-42d3-a456-426614174011";
|
||||
let initial_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("initial@example.com")
|
||||
.chatgpt_account_id(account_id),
|
||||
)?;
|
||||
let refreshed_token = encode_id_token(
|
||||
&ChatGptIdTokenClaims::new()
|
||||
.email("refreshed@example.com")
|
||||
.chatgpt_account_id(account_id),
|
||||
)?;
|
||||
let server = MockServer::start().await;
|
||||
let success = responses::sse_response(create_final_assistant_message_sse_response("done")?);
|
||||
let responses = responses::mount_response_sequence(
|
||||
&server,
|
||||
vec![success.clone(), ResponseTemplate::new(401), success],
|
||||
)
|
||||
.await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/backend-api/wham/settings/user"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"commit_attribution_enabled": false,
|
||||
})))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let codex_home = TempDir::new()?;
|
||||
let server_uri = server.uri();
|
||||
MockResponsesConfig::new(&server_uri)
|
||||
.with_root_config(&format!("chatgpt_base_url = \"{server_uri}/backend-api\""))
|
||||
.with_provider_config("requires_openai_auth = true\nsupports_websockets = false")
|
||||
.with_extra_config(&format!(
|
||||
r#"
|
||||
[model_providers.bearer_provider]
|
||||
name = "Bearer provider"
|
||||
base_url = "{server_uri}/v1"
|
||||
experimental_bearer_token = "{bearer}"
|
||||
http_headers = {{ X-Credential = "{header}" }}
|
||||
supports_websockets = false
|
||||
"#
|
||||
))
|
||||
.write(codex_home.path())?;
|
||||
write_models_cache(codex_home.path())?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.with_env_overrides(&[("OPENAI_API_KEY", None)])
|
||||
.build()
|
||||
.await?;
|
||||
let initialized = app_server
|
||||
.initialize_with_capabilities(
|
||||
ClientInfo {
|
||||
name: "codex_desktop".into(),
|
||||
title: None,
|
||||
version: "0.1.0".into(),
|
||||
},
|
||||
Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
request_attestation: true,
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
matches!(initialized, JSONRPCMessage::Response(_)),
|
||||
"initialization failed"
|
||||
);
|
||||
let login_id = app_server
|
||||
.send_chatgpt_auth_tokens_login_request(
|
||||
initial_token.clone(),
|
||||
account_id.into(),
|
||||
Some("pro".into()),
|
||||
)
|
||||
.await?;
|
||||
let _: LoginAccountResponse = app_server.read_response(login_id).await?;
|
||||
|
||||
let mut thread_ids = Vec::new();
|
||||
for provider in ["bearer_provider", "mock_provider"] {
|
||||
let thread = app_server
|
||||
.start_thread(ThreadStartParams {
|
||||
model_provider: Some(provider.into()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
app_server
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![UserInput::Text {
|
||||
text: "hello".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(Duration::from_secs(/*secs*/ 60), async {
|
||||
loop {
|
||||
match app_server.read_next_message().await? {
|
||||
JSONRPCMessage::Request(request) => {
|
||||
let (request_id, result) = match ServerRequest::try_from(request)? {
|
||||
ServerRequest::AttestationGenerate { request_id, .. } => {
|
||||
(request_id, json!({ "token": attestation }))
|
||||
}
|
||||
ServerRequest::ChatgptAuthTokensRefresh { request_id, .. } => (
|
||||
request_id,
|
||||
json!({
|
||||
"accessToken": refreshed_token,
|
||||
"chatgptAccountId": account_id,
|
||||
"chatgptPlanType": "pro",
|
||||
}),
|
||||
),
|
||||
request => anyhow::bail!("unexpected request: {request:?}"),
|
||||
};
|
||||
app_server.send_response(request_id, result).await?;
|
||||
}
|
||||
JSONRPCMessage::Notification(notification)
|
||||
if notification.method == "turn/completed" =>
|
||||
{
|
||||
let params = notification
|
||||
.params
|
||||
.context("missing turn/completed params")?;
|
||||
assert_eq!(params["turn"]["status"], "completed");
|
||||
break Ok::<_, anyhow::Error>(());
|
||||
}
|
||||
JSONRPCMessage::Error(error) => anyhow::bail!("unexpected error: {error:?}"),
|
||||
JSONRPCMessage::Response(_) | JSONRPCMessage::Notification(_) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
thread_ids.push(thread.id);
|
||||
}
|
||||
let requests = responses.requests();
|
||||
assert_eq!(
|
||||
requests
|
||||
.iter()
|
||||
.map(|request| request.header("authorization"))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
Some(format!("Bearer {bearer}")),
|
||||
Some(format!("Bearer {initial_token}")),
|
||||
Some(format!("Bearer {refreshed_token}")),
|
||||
]
|
||||
);
|
||||
assert_eq!(requests[0].header("x-credential").as_deref(), Some(header));
|
||||
assert_eq!(
|
||||
requests[2].header("x-oai-attestation"),
|
||||
Some(format!(r#"{{"v":1,"s":0,"t":"{attestation}"}}"#))
|
||||
);
|
||||
|
||||
// Wait for a later event so buffered logs cannot hide a leak.
|
||||
let barrier = "credential-log-barrier";
|
||||
app_server
|
||||
.send_response(RequestId::String(barrier.into()), json!({}))
|
||||
.await?;
|
||||
let state = StateRuntime::init(
|
||||
SqliteConfig::new_for_testing(codex_home.path().abs()),
|
||||
"mock_provider".into(),
|
||||
)
|
||||
.await?;
|
||||
let thread_ids = thread_ids.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let feedback = timeout(Duration::from_secs(/*secs*/ 60), async {
|
||||
loop {
|
||||
let logs =
|
||||
String::from_utf8(state.query_feedback_logs_for_threads(&thread_ids).await?)?;
|
||||
if logs.contains(barrier) {
|
||||
break Ok::<_, anyhow::Error>(logs);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(/*millis*/ 50)).await;
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
let persisted = format!("{:?}", state.query_logs(&LogQuery::default()).await?);
|
||||
state.close().await;
|
||||
// The HTTP assertions above prove the credentials were used. The barrier
|
||||
// confirms earlier queued logs were persisted before we check for leaks.
|
||||
for (sink, logs) in [("SQLite", persisted), ("feedback", feedback)] {
|
||||
anyhow::ensure!(logs.contains(barrier), "missing log barrier in {sink} logs");
|
||||
for secret in [
|
||||
bearer,
|
||||
header,
|
||||
&initial_token,
|
||||
&refreshed_token,
|
||||
attestation,
|
||||
] {
|
||||
anyhow::ensure!(
|
||||
!logs.contains(secret),
|
||||
"credential leaked into {sink} logs: {secret}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standalone_app_server_emits_json_info_events() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -135,7 +135,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(AttestationGenerateResponse {
|
||||
token: ATTESTATION_HEADER.to_string(),
|
||||
token: ATTESTATION_HEADER.into(),
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2578,6 +2578,12 @@ impl ProviderAuthReachabilityMode {
|
||||
}
|
||||
|
||||
fn provider_reachability_plan(config: &Config) -> ReachabilityPlan {
|
||||
let query_params = config.model_provider.query_params.as_ref().map(|params| {
|
||||
params
|
||||
.iter()
|
||||
.map(|(name, value)| (name.clone(), value.as_str().to_owned()))
|
||||
.collect::<HashMap<_, _>>()
|
||||
});
|
||||
let stored_auth = load_auth_dot_json(
|
||||
&config.codex_home,
|
||||
config.cli_auth_credentials_store_mode,
|
||||
@@ -2597,7 +2603,7 @@ fn provider_reachability_plan(config: &Config) -> ReachabilityPlan {
|
||||
&config.model_provider_id,
|
||||
&config.model_provider.name,
|
||||
config.model_provider.base_url.as_deref(),
|
||||
config.model_provider.query_params.as_ref(),
|
||||
query_params.as_ref(),
|
||||
config.model_provider.is_amazon_bedrock(),
|
||||
&config.chatgpt_base_url,
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
futures = { workspace = true, features = ["alloc", "std"] }
|
||||
gethostname = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
|
||||
use super::SessionThreadConfig;
|
||||
use super::ThreadConfigContext;
|
||||
@@ -174,15 +175,15 @@ fn model_provider_from_proto(
|
||||
base_url: provider.base_url,
|
||||
env_key: provider.env_key,
|
||||
env_key_instructions: provider.env_key_instructions,
|
||||
experimental_bearer_token: provider.experimental_bearer_token,
|
||||
experimental_bearer_token: provider.experimental_bearer_token.map(Into::into),
|
||||
auth: provider
|
||||
.auth
|
||||
.map(model_provider_auth_from_proto)
|
||||
.transpose()?,
|
||||
aws: None,
|
||||
wire_api,
|
||||
query_params: provider.query_params.map(|map| map.values),
|
||||
http_headers: provider.http_headers.map(|map| map.values),
|
||||
query_params: provider.query_params.map(redacted_string_map),
|
||||
http_headers: provider.http_headers.map(redacted_string_map),
|
||||
env_http_headers: provider.env_http_headers.map(|map| map.values),
|
||||
request_max_retries: provider.request_max_retries,
|
||||
stream_max_retries: provider.stream_max_retries,
|
||||
@@ -227,12 +228,12 @@ fn model_provider_to_proto(
|
||||
base_url,
|
||||
env_key,
|
||||
env_key_instructions,
|
||||
experimental_bearer_token,
|
||||
experimental_bearer_token: experimental_bearer_token.map(RedactedString::into_inner),
|
||||
auth: auth.map(model_provider_auth_to_proto),
|
||||
wire_api: proto_wire_api(wire_api).into(),
|
||||
query_params: query_params.map(proto_string_map),
|
||||
http_headers: http_headers.map(proto_string_map),
|
||||
env_http_headers: env_http_headers.map(proto_string_map),
|
||||
env_http_headers: env_http_headers.map(|values| proto::StringMap { values }),
|
||||
request_max_retries,
|
||||
stream_max_retries,
|
||||
stream_idle_timeout_ms,
|
||||
@@ -257,7 +258,7 @@ fn model_provider_auth_from_proto(
|
||||
|
||||
Ok(ModelProviderAuthInfo {
|
||||
command: auth.command,
|
||||
args: auth.args,
|
||||
args: auth.args.into_iter().map(RedactedString::from).collect(),
|
||||
timeout_ms,
|
||||
refresh_interval_ms: auth.refresh_interval_ms,
|
||||
cwd,
|
||||
@@ -276,16 +277,28 @@ fn model_provider_auth_to_proto(auth: ModelProviderAuthInfo) -> proto::ModelProv
|
||||
|
||||
proto::ModelProviderAuthInfo {
|
||||
command,
|
||||
args,
|
||||
args: args.into_iter().map(RedactedString::into_inner).collect(),
|
||||
timeout_ms: timeout_ms.get(),
|
||||
refresh_interval_ms,
|
||||
cwd: cwd.to_string_lossy().into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn redacted_string_map(map: proto::StringMap) -> HashMap<String, RedactedString> {
|
||||
map.values
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn proto_string_map(values: HashMap<String, String>) -> proto::StringMap {
|
||||
proto::StringMap { values }
|
||||
fn proto_string_map(values: HashMap<String, RedactedString>) -> proto::StringMap {
|
||||
proto::StringMap {
|
||||
values: values
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into_inner()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -422,7 +435,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn model_provider_proto_roundtrips_through_domain_type() {
|
||||
let expected = expected_provider();
|
||||
let mut expected = expected_provider();
|
||||
expected.auth = None;
|
||||
expected.experimental_bearer_token = Some("synthetic-provider-token".into());
|
||||
let proto = model_provider_to_proto("local", expected.clone());
|
||||
assert!(proto.supports_standalone_web_search);
|
||||
let (id, actual) = model_provider_from_proto(proto).expect("model provider from proto");
|
||||
@@ -531,7 +546,7 @@ mod tests {
|
||||
experimental_bearer_token: None,
|
||||
auth: Some(ModelProviderAuthInfo {
|
||||
command: "token-helper".to_string(),
|
||||
args: vec!["--json".to_string()],
|
||||
args: vec!["--json".into()],
|
||||
timeout_ms: NonZeroU64::new(5_000).expect("non-zero timeout"),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: workspace_dir(),
|
||||
@@ -539,12 +554,9 @@ mod tests {
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: Some(HashMap::from([(
|
||||
"api-version".to_string(),
|
||||
"2026-04-16".to_string(),
|
||||
)])),
|
||||
http_headers: Some(HashMap::from([(
|
||||
"X-Test".to_string(),
|
||||
"enabled".to_string(),
|
||||
"2026-04-16".into(),
|
||||
)])),
|
||||
http_headers: Some(HashMap::from([("X-Test".to_string(), "enabled".into())])),
|
||||
env_http_headers: Some(HashMap::from([(
|
||||
"X-Env".to_string(),
|
||||
"LOCAL_HEADER".to_string(),
|
||||
|
||||
@@ -146,6 +146,7 @@ codex-home = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-test-binary-support = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
codex-web-search-extension = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
ctor = { workspace = true }
|
||||
|
||||
@@ -998,10 +998,8 @@ env_http_headers = { "x-openai-internal-codex-residency" = "CODEX_TEST_UNSET_RES
|
||||
.as_ref()
|
||||
.expect("environment-backed headers should remain configured");
|
||||
assert_eq!(
|
||||
static_headers
|
||||
.get("X-OpenAI-Internal-Codex-Residency")
|
||||
.map(String::as_str),
|
||||
Some("request-override")
|
||||
static_headers.get("X-OpenAI-Internal-Codex-Residency"),
|
||||
Some(&"request-override".into())
|
||||
);
|
||||
assert_eq!(
|
||||
environment_headers
|
||||
@@ -1010,8 +1008,8 @@ env_http_headers = { "x-openai-internal-codex-residency" = "CODEX_TEST_UNSET_RES
|
||||
Some("CODEX_TEST_UNSET_RESIDENCY_HEADER")
|
||||
);
|
||||
assert_eq!(
|
||||
static_headers.get("x-provider-header").map(String::as_str),
|
||||
Some("preserved")
|
||||
static_headers.get("x-provider-header"),
|
||||
Some(&"preserved".into())
|
||||
);
|
||||
assert_eq!(
|
||||
environment_headers
|
||||
@@ -1140,7 +1138,7 @@ command = "print-token"
|
||||
expected_provider
|
||||
.http_headers
|
||||
.get_or_insert_default()
|
||||
.insert("X-Custom-Header".to_string(), "value".to_string());
|
||||
.insert("X-Custom-Header".to_string(), "value".into());
|
||||
|
||||
assert_eq!(config.model_provider_id, "amazon-bedrock");
|
||||
assert_eq!(config.model_provider, expected_provider);
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_config::types::FeedbackConfigToml;
|
||||
use codex_features::FeatureToml;
|
||||
use codex_login::default_client::RESIDENCY_HEADER_NAME;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// Applies managed requirements to regular config before final config construction.
|
||||
@@ -50,14 +51,12 @@ pub(super) fn apply_to_config(
|
||||
);
|
||||
if requirements.enforce_residency.value().is_some() {
|
||||
for (provider_name, provider) in &config.model_providers {
|
||||
let has_residency_header = [&provider.http_headers, &provider.env_http_headers]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|headers| {
|
||||
headers
|
||||
.keys()
|
||||
.any(|name| name.eq_ignore_ascii_case(RESIDENCY_HEADER_NAME))
|
||||
});
|
||||
let has_residency_header = provider
|
||||
.http_headers
|
||||
.iter()
|
||||
.flat_map(HashMap::keys)
|
||||
.chain(provider.env_http_headers.iter().flat_map(HashMap::keys))
|
||||
.any(|name| name.eq_ignore_ascii_case(RESIDENCY_HEADER_NAME));
|
||||
|
||||
if has_residency_header {
|
||||
let warning = format!(
|
||||
|
||||
@@ -1681,7 +1681,7 @@ fn realtime_api_key(auth: Option<&CodexAuth>, provider: &ModelProviderInfo) -> C
|
||||
}
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(token);
|
||||
return Ok(token.into_inner());
|
||||
}
|
||||
|
||||
if let Some(api_key) = auth.and_then(CodexAuth::api_key) {
|
||||
|
||||
@@ -796,7 +796,7 @@ move /y tokens.next tokens.txt >nul
|
||||
fn auth(&self) -> ModelProviderAuthInfo {
|
||||
ModelProviderAuthInfo {
|
||||
command: self.command.clone(),
|
||||
args: self.args.clone(),
|
||||
args: self.args.iter().cloned().map(Into::into).collect(),
|
||||
// Match the model-provider default to avoid brittle shell-startup timing in CI.
|
||||
timeout_ms: non_zero_u64(/*value*/ 5_000),
|
||||
refresh_interval_ms: 60_000,
|
||||
@@ -1438,7 +1438,7 @@ async fn amazon_bedrock_proxy_uses_command_auth_and_custom_headers() {
|
||||
provider
|
||||
.http_headers
|
||||
.get_or_insert_default()
|
||||
.insert("x-some-header".to_string(), "foo".to_string());
|
||||
.insert("x-some-header".to_string(), "foo".into());
|
||||
|
||||
send_request_with_provider(provider).await;
|
||||
|
||||
@@ -3585,13 +3585,13 @@ async fn azure_overrides_assign_properties_used_for_responses_url() {
|
||||
aws: None,
|
||||
query_params: Some(std::collections::HashMap::from([(
|
||||
"api-version".to_string(),
|
||||
"2025-04-01-preview".to_string(),
|
||||
"2025-04-01-preview".into(),
|
||||
)])),
|
||||
env_key_instructions: None,
|
||||
wire_api: WireApi::Responses,
|
||||
http_headers: Some(std::collections::HashMap::from([(
|
||||
"Custom-Header".to_string(),
|
||||
"Value".to_string(),
|
||||
"Value".into(),
|
||||
)])),
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
@@ -3666,7 +3666,7 @@ async fn env_var_overrides_loaded_auth() {
|
||||
env_key: Some(EXISTING_ENV_VAR_WITH_NON_EMPTY_VALUE.to_string()),
|
||||
query_params: Some(std::collections::HashMap::from([(
|
||||
"api-version".to_string(),
|
||||
"2025-04-01-preview".to_string(),
|
||||
"2025-04-01-preview".into(),
|
||||
)])),
|
||||
env_key_instructions: None,
|
||||
experimental_bearer_token: None,
|
||||
@@ -3675,7 +3675,7 @@ async fn env_var_overrides_loaded_auth() {
|
||||
wire_api: WireApi::Responses,
|
||||
http_headers: Some(std::collections::HashMap::from([(
|
||||
"Custom-Header".to_string(),
|
||||
"Value".to_string(),
|
||||
"Value".into(),
|
||||
)])),
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
|
||||
@@ -228,7 +228,7 @@ async fn responses_websocket_omits_routing_hint_for_provider_with_own_credential
|
||||
|
||||
let mut provider = websocket_provider(&server);
|
||||
provider.name = ModelProviderInfo::create_openai_provider(/*base_url*/ None).name;
|
||||
provider.experimental_bearer_token = Some("provider-specific-token".to_string());
|
||||
provider.experimental_bearer_token = Some("provider-specific-token".into());
|
||||
let harness = websocket_harness_with_provider_options_and_auth(
|
||||
provider,
|
||||
/*runtime_metrics_enabled*/ false,
|
||||
|
||||
@@ -8,6 +8,7 @@ use codex_model_provider_info::ModelProviderAwsAuthInfo;
|
||||
use codex_model_provider_info::ModelProviderInfo;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_model_provider_info::create_oss_provider_with_base_url;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
@@ -160,7 +161,7 @@ async fn custom_provider_uses_explicit_bearer_without_ambient_account() -> anyho
|
||||
);
|
||||
let mut provider =
|
||||
create_oss_provider_with_base_url(&format!("{}/v1", server.uri()), WireApi::Responses);
|
||||
provider.experimental_bearer_token = Some("provider-token".to_string());
|
||||
provider.experimental_bearer_token = Some("provider-token".into());
|
||||
let mut builder = test_codex()
|
||||
.with_auth(CodexAuth::Headers(AuthHeaders::new(headers)))
|
||||
.with_config(move |config| {
|
||||
@@ -253,7 +254,9 @@ async fn amazon_bedrock_aws_auth_refresh_resigns() -> anyhow::Result<()> {
|
||||
region: Some("us-east-1".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: Vec::from(["--exact", TEST_NAME, "--skip", HELPER_ARG].map(str::to_string)),
|
||||
args: Vec::from(
|
||||
["--exact", TEST_NAME, "--skip", HELPER_ARG].map(RedactedString::from),
|
||||
),
|
||||
timeout_ms: NonZeroU64::new(30_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -365,7 +365,7 @@ async fn responses_lite_exposes_standalone_tools_for_actor_authorized_provider()
|
||||
config.model_provider.requires_openai_auth = false;
|
||||
config.model_provider.http_headers = Some(HashMap::from([(
|
||||
"x-openai-actor-authorization".to_string(),
|
||||
"test-actor-authorization".to_string(),
|
||||
"test-actor-authorization".into(),
|
||||
)]));
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::manager::ExternalAuthRefreshContext;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
@@ -103,7 +104,7 @@ async fn run_provider_auth_command(config: &ModelProviderAuthInfo) -> io::Result
|
||||
let program = resolve_provider_auth_program(&config.command, &config.cwd)?;
|
||||
let mut command = Command::new(&program);
|
||||
command
|
||||
.args(&config.args)
|
||||
.args(config.args.iter().map(Deref::deref))
|
||||
.current_dir(config.cwd.as_path())
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
|
||||
@@ -15,6 +15,7 @@ workspace = true
|
||||
[dependencies]
|
||||
codex-api = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
http = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::EnvVarError;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
use http::HeaderMap;
|
||||
use http::header::HeaderName;
|
||||
use http::header::HeaderValue;
|
||||
@@ -107,7 +108,7 @@ pub struct ModelProviderInfo {
|
||||
/// Value to use with `Authorization: Bearer <token>` header. Use of this
|
||||
/// config is discouraged in favor of `env_key` for security reasons, but
|
||||
/// this may be necessary when using this programmatically.
|
||||
pub experimental_bearer_token: Option<String>,
|
||||
pub experimental_bearer_token: Option<RedactedString>,
|
||||
/// Command-backed bearer-token configuration for this provider.
|
||||
pub auth: Option<ModelProviderAuthInfo>,
|
||||
/// AWS SigV4 auth configuration for this provider.
|
||||
@@ -116,10 +117,10 @@ pub struct ModelProviderInfo {
|
||||
#[serde(default)]
|
||||
pub wire_api: WireApi,
|
||||
/// Optional query parameters to append to the base URL.
|
||||
pub query_params: Option<HashMap<String, String>>,
|
||||
pub query_params: Option<HashMap<String, RedactedString>>,
|
||||
/// Additional HTTP headers to include in requests to this provider where
|
||||
/// the (key, value) pairs are the header name and value.
|
||||
pub http_headers: Option<HashMap<String, String>>,
|
||||
pub http_headers: Option<HashMap<String, RedactedString>>,
|
||||
/// Optional HTTP headers to include in requests to this provider where the
|
||||
/// (key, value) pairs are the header name and _environment variable_ whose
|
||||
/// value should be used. If the environment variable is not set, or the
|
||||
@@ -169,7 +170,7 @@ pub struct AwsAuthRefreshConfig {
|
||||
pub command: String,
|
||||
/// Arguments passed to the refresh command.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub args: Vec<RedactedString>,
|
||||
/// Maximum time to wait for the refresh command to complete.
|
||||
#[serde(default = "default_aws_auth_refresh_timeout_ms")]
|
||||
pub timeout_ms: NonZeroU64,
|
||||
@@ -265,7 +266,9 @@ impl ModelProviderInfo {
|
||||
let mut headers = HeaderMap::with_capacity(capacity);
|
||||
if let Some(extra) = &self.http_headers {
|
||||
for (k, v) in extra {
|
||||
if let (Ok(name), Ok(value)) = (HeaderName::try_from(k), HeaderValue::try_from(v)) {
|
||||
if let (Ok(name), Ok(value)) =
|
||||
(HeaderName::try_from(k), HeaderValue::try_from(v.as_str()))
|
||||
{
|
||||
headers.insert(name, value);
|
||||
}
|
||||
}
|
||||
@@ -318,7 +321,12 @@ impl ModelProviderInfo {
|
||||
Ok(ApiProvider {
|
||||
name: self.name.clone(),
|
||||
base_url,
|
||||
query_params: self.query_params.clone(),
|
||||
query_params: self.query_params.clone().map(|params| {
|
||||
params
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into_inner()))
|
||||
.collect()
|
||||
}),
|
||||
headers,
|
||||
retry,
|
||||
stream_idle_timeout: self.stream_idle_timeout(),
|
||||
@@ -386,7 +394,7 @@ impl ModelProviderInfo {
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: Some(
|
||||
[("version".to_string(), env!("CARGO_PKG_VERSION").to_string())]
|
||||
[("version".to_string(), env!("CARGO_PKG_VERSION").into())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
@@ -434,7 +442,7 @@ impl ModelProviderInfo {
|
||||
query_params: None,
|
||||
http_headers: Some(HashMap::from([(
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_HEADER.to_string(),
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_VALUE.to_string(),
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_VALUE.into(),
|
||||
)])),
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
|
||||
@@ -54,7 +54,7 @@ query_params = { api-version = "2025-04-01-preview" }
|
||||
aws: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: Some(maplit::hashmap! {
|
||||
"api-version".to_string() => "2025-04-01-preview".to_string(),
|
||||
"api-version".to_string() => "2025-04-01-preview".into(),
|
||||
}),
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
@@ -92,7 +92,7 @@ supports_standalone_web_search = true
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: Some(maplit::hashmap! {
|
||||
"X-Example-Header".to_string() => "example-value".to_string(),
|
||||
"X-Example-Header".to_string() => "example-value".into(),
|
||||
}),
|
||||
env_http_headers: Some(maplit::hashmap! {
|
||||
"X-Example-Env-Header".to_string() => "EXAMPLE_ENV_VAR".to_string(),
|
||||
@@ -158,7 +158,7 @@ fn test_header_auth_uses_chatgpt_codex_base_url() {
|
||||
fn test_uses_openai_actor_authorization() {
|
||||
let mut provider = ModelProviderInfo {
|
||||
http_headers: Some(maplit::hashmap! {
|
||||
"X-OpenAI-Actor-Authorization".to_string() => "actor-token".to_string(),
|
||||
"X-OpenAI-Actor-Authorization".to_string() => "actor-token".into(),
|
||||
}),
|
||||
..ModelProviderInfo::default()
|
||||
};
|
||||
@@ -168,12 +168,12 @@ fn test_uses_openai_actor_authorization() {
|
||||
assert!(!provider.uses_openai_actor_authorization());
|
||||
|
||||
provider.http_headers = Some(maplit::hashmap! {
|
||||
OPENAI_ACTOR_AUTHORIZATION_HEADER.to_string() => " ".to_string(),
|
||||
OPENAI_ACTOR_AUTHORIZATION_HEADER.to_string() => " ".into(),
|
||||
});
|
||||
assert!(!provider.uses_openai_actor_authorization());
|
||||
|
||||
provider.http_headers = Some(maplit::hashmap! {
|
||||
OPENAI_ACTOR_AUTHORIZATION_HEADER.to_string() => "actor-token".to_string(),
|
||||
OPENAI_ACTOR_AUTHORIZATION_HEADER.to_string() => "actor-token".into(),
|
||||
});
|
||||
provider.requires_openai_auth = true;
|
||||
assert!(!provider.uses_openai_actor_authorization());
|
||||
@@ -199,7 +199,7 @@ args = ["--format=text"]
|
||||
provider.auth,
|
||||
Some(ModelProviderAuthInfo {
|
||||
command: "./scripts/print-token".to_string(),
|
||||
args: vec!["--format=text".to_string()],
|
||||
args: vec!["--format=text".into()],
|
||||
timeout_ms: NonZeroU64::new(5_000).unwrap(),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: AbsolutePathBuf::resolve_path_against_base(".", base_dir.path()),
|
||||
@@ -231,11 +231,7 @@ args = ["login", "--profile", "codex-bedrock"]
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec![
|
||||
"login".to_string(),
|
||||
"--profile".to_string(),
|
||||
"codex-bedrock".to_string(),
|
||||
],
|
||||
args: vec!["login".into(), "--profile".into(), "codex-bedrock".into()],
|
||||
timeout_ms: NonZeroU64::new(300_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
})
|
||||
@@ -262,7 +258,7 @@ fn test_create_amazon_bedrock_provider() {
|
||||
query_params: None,
|
||||
http_headers: Some(maplit::hashmap! {
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_HEADER.to_string() =>
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_VALUE.to_string(),
|
||||
AMAZON_BEDROCK_MANTLE_CLIENT_AGENT_VALUE.into(),
|
||||
}),
|
||||
env_http_headers: None,
|
||||
request_max_retries: None,
|
||||
@@ -320,7 +316,7 @@ fn test_create_amazon_bedrock_runtime_provider_with_aws_configuration() {
|
||||
fn provider_auth_for_test() -> ModelProviderAuthInfo {
|
||||
ModelProviderAuthInfo {
|
||||
command: "token-fetcher".to_string(),
|
||||
args: vec!["fetch".to_string()],
|
||||
args: vec!["fetch".into()],
|
||||
timeout_ms: NonZeroU64::new(5_000).expect("timeout should be non-zero"),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: std::env::current_dir()
|
||||
@@ -408,11 +404,7 @@ fn test_merge_configured_model_providers_adds_custom_provider() {
|
||||
fn test_merge_configured_model_providers_applies_amazon_bedrock_aws_override() {
|
||||
let auth_refresh = AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec![
|
||||
"login".to_string(),
|
||||
"--profile".to_string(),
|
||||
"codex-bedrock".to_string(),
|
||||
],
|
||||
args: vec!["login".into(), "--profile".into(), "codex-bedrock".into()],
|
||||
timeout_ms: NonZeroU64::new(10_000).expect("timeout should be non-zero"),
|
||||
};
|
||||
let configured_model_providers = std::collections::HashMap::from([(
|
||||
@@ -491,7 +483,7 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_transport_overri
|
||||
auth_refresh: None,
|
||||
}),
|
||||
http_headers: Some(maplit::hashmap! {
|
||||
"x-example-header".to_string() => "value".to_string(),
|
||||
"x-example-header".to_string() => "value".into(),
|
||||
}),
|
||||
..ModelProviderInfo::default()
|
||||
},
|
||||
@@ -511,7 +503,7 @@ fn test_merge_configured_model_providers_applies_amazon_bedrock_transport_overri
|
||||
expected_provider
|
||||
.http_headers
|
||||
.get_or_insert_default()
|
||||
.insert("x-example-header".to_string(), "value".to_string());
|
||||
.insert("x-example-header".to_string(), "value".into());
|
||||
|
||||
assert_eq!(
|
||||
merge_configured_model_providers(
|
||||
|
||||
@@ -29,6 +29,7 @@ tokio = { workspace = true, features = ["process", "sync", "time"] }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::io::IsTerminal;
|
||||
use std::ops::Deref;
|
||||
use std::process::Stdio;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -44,7 +45,7 @@ impl AwsAuthRecovery {
|
||||
|
||||
let mut command = Command::new(&self.config.command);
|
||||
command
|
||||
.args(&self.config.args)
|
||||
.args(self.config.args.iter().map(Deref::deref))
|
||||
.stdin(if std::io::stdin().is_terminal() {
|
||||
Stdio::inherit()
|
||||
} else {
|
||||
|
||||
@@ -318,7 +318,7 @@ mod tests {
|
||||
provider.base_url = base_url.map(str::to_string);
|
||||
provider.auth = Some(ModelProviderAuthInfo {
|
||||
command: "token-fetcher".to_string(),
|
||||
args: vec!["fetch".to_string()],
|
||||
args: vec!["fetch".into()],
|
||||
timeout_ms: NonZeroU64::new(5_000).expect("timeout should be non-zero"),
|
||||
refresh_interval_ms: 300_000,
|
||||
cwd: std::env::current_dir()
|
||||
@@ -353,7 +353,7 @@ mod tests {
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec!["login".to_string()],
|
||||
args: vec!["login".into()],
|
||||
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
});
|
||||
@@ -397,7 +397,7 @@ mod tests {
|
||||
region: Some("us-west-2".to_string()),
|
||||
auth_refresh: Some(AwsAuthRefreshConfig {
|
||||
command: "aws".to_string(),
|
||||
args: vec!["login".to_string()],
|
||||
args: vec!["login".into()],
|
||||
timeout_ms: NonZeroU64::new(1_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
})),
|
||||
|
||||
@@ -294,7 +294,7 @@ fn bearer_auth_for_provider(
|
||||
}
|
||||
|
||||
if let Some(token) = provider.experimental_bearer_token.clone() {
|
||||
return Ok(Some(BearerAuthProvider::new(token)));
|
||||
return Ok(Some(BearerAuthProvider::new(token.into_inner())));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -512,7 +512,7 @@ mod tests {
|
||||
fn custom_provider_uses_explicit_bearer_instead_of_ambient_auth() {
|
||||
let mut provider =
|
||||
create_oss_provider_with_base_url("http://localhost:11434/v1", WireApi::Responses);
|
||||
provider.experimental_bearer_token = Some("provider-token".to_string());
|
||||
provider.experimental_bearer_token = Some("provider-token".into());
|
||||
let ambient_auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth {
|
||||
api_key: "bedrock-api-key-test".to_string(),
|
||||
region: "us-east-1".to_string(),
|
||||
|
||||
@@ -413,7 +413,7 @@ mod tests {
|
||||
let mut provider_info = ModelProviderInfo::create_openai_provider(Some(server.uri()));
|
||||
provider_info.http_headers = Some(std::collections::HashMap::from([(
|
||||
RESIDENCY_HEADER_NAME.to_string(),
|
||||
"eu".to_string(),
|
||||
"eu".into(),
|
||||
)]));
|
||||
let endpoint = OpenAiModelsEndpoint {
|
||||
provider_info,
|
||||
@@ -438,9 +438,8 @@ mod tests {
|
||||
.provider_info
|
||||
.http_headers
|
||||
.as_ref()
|
||||
.and_then(|headers| headers.get(RESIDENCY_HEADER_NAME))
|
||||
.map(String::as_str),
|
||||
Some("eu")
|
||||
.and_then(|headers| headers.get(RESIDENCY_HEADER_NAME)),
|
||||
Some(&"eu".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,7 @@ mod tests {
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use wiremock::Mock;
|
||||
@@ -821,7 +822,7 @@ mod tests {
|
||||
"--skip",
|
||||
counter.to_str().expect("counter path should be UTF-8"),
|
||||
]
|
||||
.map(str::to_string),
|
||||
.map(RedactedString::from),
|
||||
),
|
||||
timeout_ms: NonZeroU64::new(10_000).expect("timeout should be non-zero"),
|
||||
}),
|
||||
@@ -1169,7 +1170,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let mut provider_info = provider_for(server.uri());
|
||||
provider_info.experimental_bearer_token = Some("provider-token".to_string());
|
||||
provider_info.experimental_bearer_token = Some("provider-token".into());
|
||||
let provider = create_model_provider(
|
||||
provider_info,
|
||||
Some(AuthManager::from_auth_for_testing(
|
||||
|
||||
@@ -23,6 +23,7 @@ codex-network-proxy = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-image = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-redacted-string = { workspace = true }
|
||||
codex-utils-string = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_redacted_string::RedactedString;
|
||||
use schemars::JsonSchema;
|
||||
use schemars::r#gen::SchemaGenerator;
|
||||
use schemars::schema::InstanceType;
|
||||
@@ -571,7 +572,7 @@ pub struct ModelProviderAuthInfo {
|
||||
|
||||
/// Command arguments.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub args: Vec<RedactedString>,
|
||||
|
||||
/// Maximum time to wait for the token command to exit successfully.
|
||||
#[serde(default = "default_provider_auth_timeout_ms")]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "redacted-string",
|
||||
crate_name = "codex_utils_redacted_string",
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "codex-utils-redacted-string"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[lib]
|
||||
test = false
|
||||
doctest = false
|
||||
@@ -0,0 +1,49 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::ops::DerefMut;
|
||||
|
||||
/// A string whose `Debug` output is redacted.
|
||||
#[derive(Clone, Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct RedactedString(String);
|
||||
|
||||
impl RedactedString {
|
||||
pub fn into_inner(self) -> String {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RedactedString {
|
||||
type Target = String;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for RedactedString {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for RedactedString {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RedactedString {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for RedactedString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("<redacted>")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user