diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index db7b940a89..c944e5db9c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -16553,7 +16553,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "protocol": { @@ -16596,7 +16596,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "tls": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 38d9613415..79920c59cb 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -12545,7 +12545,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "protocol": { @@ -12588,7 +12588,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "tls": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index 2df50dbc7b..6f0b285cde 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -1169,7 +1169,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "protocol": { @@ -1212,7 +1212,7 @@ "additionalProperties": { "type": "string" }, - "description": "Exporter request headers.", + "description": "Header names are retained; values are redacted by the server.", "type": "object" }, "tls": { diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 9920c48d60..21124b8714 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 972259a204..133b65b61a 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/OtelExporter.ts b/codex-rs/app-server-protocol/schema/typescript/v2/OtelExporter.ts index bc0df2c2c7..f18fa658c4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/OtelExporter.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/OtelExporter.ts @@ -6,10 +6,10 @@ import type { OtelTlsConfig } from "./OtelTlsConfig"; export type OtelExporter = "none" | "statsig" | { "otlp-http": { endpoint: string, /** - * Exporter request headers. + * Header names are retained; values are redacted by the server. */ headers: { [key in string]?: string }, protocol: OtelHttpProtocol, tls: OtelTlsConfig | null, } } | { "otlp-grpc": { endpoint: string, /** - * Exporter request headers. + * Header names are retained; values are redacted by the server. */ headers: { [key in string]?: string }, tls: OtelTlsConfig | null, } }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index c8364ae2e5..444fffde86 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -19,6 +19,7 @@ use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; +pub use codex_utils_redacted_string::SerdeRedactedString; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -529,15 +530,19 @@ pub enum OtelExporter { Statsig, OtlpHttp { endpoint: String, - /// Exporter request headers. - headers: HashMap, + /// Header names are retained; values are redacted by the server. + #[schemars(with = "HashMap")] + #[ts(as = "HashMap")] + headers: HashMap, protocol: OtelHttpProtocol, tls: Option, }, OtlpGrpc { endpoint: String, - /// Exporter request headers. - headers: HashMap, + /// Header names are retained; values are redacted by the server. + #[schemars(with = "HashMap")] + #[ts(as = "HashMap")] + headers: HashMap, tls: Option, }, } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 71135843bb..ad2ea0028d 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -708,3 +708,6 @@ the app-server host and retry setup. `configRequirements/read` exposes managed `otel` settings. `config/read` applies those settings to the effective configuration and omits ordinary origins for managed fields. Configuration writes that overlap managed settings are rejected. + +Exporter header values are redacted in effective config, source layers, managed +requirements, and write-override metadata. Runtime exporters retain the original values. diff --git a/codex-rs/app-server/src/config_manager_service.rs b/codex-rs/app-server/src/config_manager_service.rs index ebb3b5efc5..b1cbf6951f 100644 --- a/codex-rs/app-server/src/config_manager_service.rs +++ b/codex-rs/app-server/src/config_manager_service.rs @@ -1,6 +1,7 @@ use crate::config_layer::config_layer_metadata_to_api; use crate::config_layer::config_layer_to_api; use crate::config_manager::ConfigManager; +use crate::otel_config::redact_otel_exporter_headers; use codex_app_server_protocol::Config as ApiConfig; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigEdit as ApiConfigEdit; @@ -168,7 +169,7 @@ impl ConfigManager { !matches!(&metadata.name, ConfigLayerSource::PackagedDefaults { .. }) }); - Ok(ConfigReadResponse { + let mut response = ConfigReadResponse { config, origins: origins .into_iter() @@ -183,7 +184,20 @@ impl ConfigManager { .map(|layer| config_layer_to_api(layer.as_layer())) .collect() }), - }) + }; + + // Redact exporter credentials from both the effective config and any + // included config layers before returning the public API response. + if let Some(otel) = response.config.additional.get_mut("otel") { + redact_otel_exporter_headers(otel); + } + for layer in response.layers.iter_mut().flatten() { + if let Some(otel) = layer.config.get_mut("otel") { + redact_otel_exporter_headers(otel); + } + } + + Ok(response) } pub(crate) async fn read_requirements( @@ -952,12 +966,28 @@ fn compute_override_metadata( } let message = override_message(&overriding_layer.name); + // Redact the complete OTEL config before extracting the overridden path, + // which may point to an entire exporter or an individual credential. + let effective_value = if segments.first().is_some_and(|segment| segment == "otel") { + effective + .get("otel") + .and_then(|otel| serde_json::to_value(otel).ok()) + .and_then(|mut otel| { + redact_otel_exporter_headers(&mut otel); + segments[1..] + .iter() + .try_fold(&otel, |value, segment| value.get(segment.as_str())) + .cloned() + }) + } else { + effective_value.and_then(|value| serde_json::to_value(value).ok()) + } + .unwrap_or(JsonValue::Null); + Some(OverriddenMetadata { message, overriding_layer: config_layer_metadata_to_api(overriding_layer), - effective_value: effective_value - .and_then(|value| serde_json::to_value(value).ok()) - .unwrap_or(JsonValue::Null), + effective_value, }) } diff --git a/codex-rs/app-server/src/config_manager_service_tests.rs b/codex-rs/app-server/src/config_manager_service_tests.rs index f8e17eff3c..fa443cb4ed 100644 --- a/codex-rs/app-server/src/config_manager_service_tests.rs +++ b/codex-rs/app-server/src/config_manager_service_tests.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::ConfigLayerSource as ApiConfigLayerSource; use codex_config::CloudConfigBundleLoader; use codex_config::LoaderOverrides; use codex_config::test_support::CloudConfigBundleFixture; +use codex_config::types::OtelExporterKind; use codex_http_client::HttpClientFactory; use codex_http_client::OutboundProxyPolicy; use codex_utils_absolute_path::AbsolutePathBuf; @@ -2101,6 +2102,178 @@ async fn write_value_reports_managed_override() { assert_eq!(overridden.effective_value, serde_json::json!("never")); } +#[tokio::test] +async fn write_overrides_redact_managed_otel_exporter_headers() { + let tmp = tempdir().expect("tempdir"); + let user_path = tmp.path().join(CONFIG_TOML_FILE); + std::fs::write(&user_path, "").expect("write user config"); + let managed_path = tmp.path().join("managed_config.toml"); + std::fs::write( + &managed_path, + r#"[otel] +environment = "managed" +exporter = { otlp-http = { endpoint = "https://collector.example", protocol = "json", headers = { Authorization = "admin-secret", "x.api.key" = "dotted-secret" } } } +trace_exporter = { otlp-grpc = { endpoint = "https://trace.example", headers = { Authorization = "trace-secret" } } } +metrics_exporter = { otlp-http = { endpoint = "https://metrics.example", protocol = "json", headers = { Authorization = "metrics-secret" } } } +span_attributes = { "service.name" = "managed-service" }"#, + ) + .expect("write managed config"); + let service = ConfigManager::new_for_tests( + tmp.path().to_path_buf(), + vec![], + LoaderOverrides::with_managed_config_path_for_tests(managed_path), + CloudConfigBundleLoader::default(), + ); + let exporter = serde_json::json!({ + "otlp-http": { + "endpoint": "https://collector.example", + "headers": { "Authorization": "[redacted]", "x.api.key": "[redacted]" }, + "protocol": "json" + } + }); + let otel = serde_json::json!({ + "environment": "managed", + "exporter": exporter.clone(), + "trace_exporter": { + "otlp-grpc": { + "endpoint": "https://trace.example", + "headers": { "Authorization": "[redacted]" } + } + }, + "metrics_exporter": { + "otlp-http": { + "endpoint": "https://metrics.example", + "protocol": "json", + "headers": { "Authorization": "[redacted]" } + } + }, + "span_attributes": { "service.name": "managed-service" } + }); + + for (key_path, expected) in [ + ("otel", otel.clone()), + ("otel.exporter", exporter.clone()), + ("otel.exporter.otlp-http", exporter["otlp-http"].clone()), + ( + "otel.exporter.otlp-http.headers", + exporter["otlp-http"]["headers"].clone(), + ), + ( + "otel.exporter.otlp-http.headers.Authorization", + serde_json::json!("[redacted]"), + ), + ( + "otel.exporter.otlp-http.headers.\"x.api.key\"", + serde_json::json!("[redacted]"), + ), + ( + "otel.exporter.otlp-http.endpoint", + serde_json::json!("https://collector.example"), + ), + ( + "otel.exporter.otlp-http.protocol", + serde_json::json!("json"), + ), + ("otel.trace_exporter", otel["trace_exporter"].clone()), + ("otel.metrics_exporter", otel["metrics_exporter"].clone()), + ( + "otel.metrics_exporter.otlp-http.headers.Authorization", + serde_json::json!("[redacted]"), + ), + ("otel.environment", serde_json::json!("managed")), + ( + "otel.span_attributes.\"service.name\"", + serde_json::json!("managed-service"), + ), + ] { + let response = service + .write_value(ConfigValueWriteParams { + file_path: None, + key_path: key_path.to_string(), + value: serde_json::Value::Null, + merge_strategy: MergeStrategy::Replace, + expected_version: None, + }) + .await + .expect("no-op config clear should succeed"); + assert_eq!(response.status, WriteStatus::OkOverridden, "{key_path}"); + assert_eq!( + response + .overridden_metadata + .expect("managed override") + .effective_value, + expected, + "{key_path}" + ); + assert_eq!(std::fs::read_to_string(&user_path).unwrap(), ""); + } + + let response = service + .batch_write(ConfigBatchWriteParams { + edits: vec![codex_app_server_protocol::ConfigEdit { + key_path: "otel.exporter.otlp-http.headers.Authorization".to_string(), + value: serde_json::Value::Null, + merge_strategy: MergeStrategy::Replace, + }], + file_path: None, + expected_version: None, + reload_user_config: false, + }) + .await + .expect("no-op batch clear should succeed"); + assert_eq!(response.status, WriteStatus::OkOverridden); + assert_eq!( + response + .overridden_metadata + .expect("managed batch override") + .effective_value, + serde_json::json!("[redacted]") + ); + assert_eq!(std::fs::read_to_string(&user_path).unwrap(), ""); + + let read = service + .read(ConfigReadParams { + include_layers: true, + cwd: None, + }) + .await + .expect("read public config"); + let mut effective_exporter = exporter.clone(); + effective_exporter["otlp-http"]["tls"] = serde_json::Value::Null; + assert_eq!( + read.config.additional["otel"]["exporter"], + effective_exporter + ); + let managed_layer = read + .layers + .expect("config layers") + .into_iter() + .find(|layer| { + matches!( + layer.name, + ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } + ) + }) + .expect("legacy managed layer"); + assert_eq!(managed_layer.config["otel"]["exporter"], exporter); + + let runtime = service + .load_latest_config(/*fallback_cwd*/ None) + .await + .expect("load runtime config"); + let OtelExporterKind::OtlpHttp { headers, .. } = runtime.otel.exporter else { + panic!("unexpected runtime exporter"); + }; + assert_eq!( + headers.get("Authorization").map(|value| value.as_str()), + Some("admin-secret") + ); + assert_eq!( + headers.get("x.api.key").map(|value| value.as_str()), + Some("dotted-secret") + ); +} + /// Legacy managed feature toggles own their normalized enabled origin and override metadata. #[tokio::test] async fn multi_agent_v2_boolean_layer_owns_enabled_origin_and_overrides() { diff --git a/codex-rs/app-server/src/otel_config.rs b/codex-rs/app-server/src/otel_config.rs index b9f5122d35..12b79b01e2 100644 --- a/codex-rs/app-server/src/otel_config.rs +++ b/codex-rs/app-server/src/otel_config.rs @@ -1,14 +1,17 @@ -//! Maps OTEL requirements into app-server responses. +//! Maps OTEL requirements and masks exporter credentials in app-server responses. use codex_app_server_protocol::OtelExporter; use codex_app_server_protocol::OtelHttpProtocol; use codex_app_server_protocol::OtelRequirements; use codex_app_server_protocol::OtelTlsConfig; use codex_app_server_protocol::OtelToolResultLogConfig; +use codex_app_server_protocol::SerdeRedactedString; use codex_config::OtelRequirementsToml; use codex_config::types::OtelExporterKind; use codex_config::types::OtelHttpProtocol as CoreOtelHttpProtocol; use codex_config::types::OtelTlsConfig as CoreOtelTlsConfig; +use serde_json::Value as JsonValue; +use std::collections::HashMap; pub(crate) fn map_otel_requirements_to_api(otel: OtelRequirementsToml) -> OtelRequirements { OtelRequirements { @@ -26,6 +29,12 @@ pub(crate) fn map_otel_requirements_to_api(otel: OtelRequirementsToml) -> OtelRe } fn map_otel_exporter_to_api(exporter: OtelExporterKind) -> OtelExporter { + let response_headers = |headers: HashMap| { + headers + .into_iter() + .map(|(name, value)| (name, SerdeRedactedString::from(value))) + .collect() + }; match exporter { OtelExporterKind::None => OtelExporter::None, OtelExporterKind::Statsig => OtelExporter::Statsig, @@ -36,7 +45,7 @@ fn map_otel_exporter_to_api(exporter: OtelExporterKind) -> OtelExporter { tls, } => OtelExporter::OtlpHttp { endpoint, - headers, + headers: response_headers(headers), protocol: match protocol { CoreOtelHttpProtocol::Binary => OtelHttpProtocol::Binary, CoreOtelHttpProtocol::Json => OtelHttpProtocol::Json, @@ -49,7 +58,7 @@ fn map_otel_exporter_to_api(exporter: OtelExporterKind) -> OtelExporter { tls, } => OtelExporter::OtlpGrpc { endpoint, - headers, + headers: response_headers(headers), tls: tls.map(map_otel_tls_to_api), }, } @@ -62,3 +71,22 @@ fn map_otel_tls_to_api(tls: CoreOtelTlsConfig) -> OtelTlsConfig { client_private_key: tls.client_private_key.map(Into::into), } } + +/// Masks exporter header values before OTEL configuration crosses the API boundary. +pub(crate) fn redact_otel_exporter_headers(otel: &mut JsonValue) { + for field in ["exporter", "trace_exporter", "metrics_exporter"] { + let Some(exporter) = otel.get_mut(field).and_then(JsonValue::as_object_mut) else { + continue; + }; + + for config in exporter.values_mut() { + let Some(headers) = config.get_mut("headers").and_then(JsonValue::as_object_mut) else { + continue; + }; + + for value in headers.values_mut() { + *value = SerdeRedactedString::PLACEHOLDER.into(); + } + } + } +} diff --git a/codex-rs/app-server/tests/suite/v2/config_rpc.rs b/codex-rs/app-server/tests/suite/v2/config_rpc.rs index 7e271bad87..fcaa7759d4 100644 --- a/codex-rs/app-server/tests/suite/v2/config_rpc.rs +++ b/codex-rs/app-server/tests/suite/v2/config_rpc.rs @@ -602,7 +602,7 @@ async fn config_read_disables_guardian_v2_when_managed_config_requires_guardian_ #[test_case::test_case("json", "none"; "json_and_disabled_metrics")] #[test_case::test_case("binary", "statsig"; "binary_and_statsig_metrics")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn config_requirements_read_includes_complete_otel_policy_and_headers( +async fn config_requirements_read_redacts_otel_exporter_headers( protocol: &str, metrics_exporter: &str, ) -> Result<()> { @@ -641,7 +641,7 @@ metrics_exporter = "$METRICS_EXPORTER""# "exporter": { "otlp-http": { "endpoint": "http://127.0.0.1:1/v1/logs", - "headers": { "Authorization": "Bearer managed-token" }, + "headers": { "Authorization": "[redacted]" }, "protocol": protocol, "tls": { "ca-certificate": codex_home.path().join("ca.pem"), @@ -653,7 +653,7 @@ metrics_exporter = "$METRICS_EXPORTER""# "trace_exporter": { "otlp-grpc": { "endpoint": "http://127.0.0.1:4317", - "headers": { "x-api-key": "managed-grpc-secret" }, + "headers": { "x-api-key": "[redacted]" }, "tls": { "ca-certificate": codex_home.path().join("ca.pem"), "client-certificate": null, @@ -682,6 +682,73 @@ metrics_exporter = "$METRICS_EXPORTER""# Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn config_read_redacts_otel_exporter_headers_from_effective_config_and_layers() -> Result<()> +{ + let codex_home = TempDir::new()?; + write_config( + &codex_home, + r#"[otel] +exporter = { otlp-http = { endpoint = "http://127.0.0.1:1/v1/logs", protocol = "json", headers = { Authorization = "logs-secret" } } } +trace_exporter = { otlp-grpc = { endpoint = "http://127.0.0.1:1", headers = { Authorization = "traces-secret" } } } +metrics_exporter = { otlp-http = { endpoint = "http://127.0.0.1:1/v1/metrics", protocol = "binary", headers = { Authorization = "metrics-secret" } } }"#, + )?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .without_auto_env() + .with_args(&[ + "-c", + "otel.exporter.otlp-http.headers.Authorization='session-secret'", + ]) + .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) + .await?; + let request_id = mcp + .send_config_read_request(ConfigReadParams { + include_layers: true, + cwd: None, + }) + .await?; + let response: ConfigReadResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; + let serialized = serde_json::to_string(&response)?; + let effective_otel = response + .config + .additional + .get("otel") + .expect("effective OTEL config"); + let layers = response.layers.as_ref().expect("included config layers"); + let user_otel = layers + .iter() + .find(|layer| matches!(&layer.name, ConfigLayerSource::User { .. })) + .and_then(|layer| layer.config.get("otel")) + .expect("user OTEL config layer"); + + let flags = layers + .iter() + .find(|layer| matches!(layer.name, ConfigLayerSource::SessionFlags)) + .expect("session flags"); + assert_eq!( + flags.config["otel"], + json!({ + "exporter": { "otlp-http": { "headers": { "Authorization": "[redacted]" } } } + }) + ); + assert!(!serialized.contains("session-secret")); + + for (field, transport, secret) in [ + ("exporter", "otlp-http", "logs-secret"), + ("trace_exporter", "otlp-grpc", "traces-secret"), + ("metrics_exporter", "otlp-http", "metrics-secret"), + ] { + let expected = json!({ "Authorization": "[redacted]" }); + assert_eq!(effective_otel[field][transport]["headers"], expected); + assert_eq!(user_otel[field][transport]["headers"], expected); + assert!(!serialized.contains(secret), "response leaked {field}"); + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn config_read_returns_effective_and_layers() -> Result<()> { let codex_home = TempDir::new()?; diff --git a/codex-rs/app-server/tests/suite/v2/otel.rs b/codex-rs/app-server/tests/suite/v2/otel.rs index 144b1d4e33..a5ec203eb4 100644 --- a/codex-rs/app-server/tests/suite/v2/otel.rs +++ b/codex-rs/app-server/tests/suite/v2/otel.rs @@ -130,6 +130,18 @@ async fn account_switch_reloads_telemetry_collectors_and_preserves_trace_context .received_requests() .await .context("collector did not record requests")?; + for request in requests + .iter() + .filter(|request| request.url.path().ends_with("/logs")) + { + assert_eq!( + request + .headers + .get("x-collector-key") + .and_then(|value| value.to_str().ok()), + Some("test-collector-key") + ); + } let exported = |path: &str| { requests .iter() @@ -189,7 +201,7 @@ enabled = false [otel] environment = "test" -exporter = {{ otlp-http = {{ endpoint = "{collector_endpoint}/logs", protocol = "json" }} }} +exporter = {{ otlp-http = {{ endpoint = "{collector_endpoint}/logs", protocol = "json", headers = {{ x-collector-key = "test-collector-key" }} }} }} trace_exporter = {{ otlp-http = {{ endpoint = "{collector_endpoint}/traces", protocol = "json" }} }} metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_endpoint}/metrics", protocol = "json" }} }} "# diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 15ede5c942..3edd406c5b 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -30,6 +30,7 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::fmt; +use codex_utils_redacted_string::RedactedString; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -616,7 +617,8 @@ pub enum OtelExporterKind { OtlpHttp { endpoint: String, #[serde(default)] - headers: HashMap, + #[schemars(with = "HashMap")] + headers: HashMap, protocol: OtelHttpProtocol, #[serde(default)] tls: Option, @@ -624,7 +626,8 @@ pub enum OtelExporterKind { OtlpGrpc { endpoint: String, #[serde(default)] - headers: HashMap, + #[schemars(with = "HashMap")] + headers: HashMap, #[serde(default)] tls: Option, }, diff --git a/codex-rs/core/src/otel_init.rs b/codex-rs/core/src/otel_init.rs index a6b6a70ec0..1fc9a05abe 100644 --- a/codex-rs/core/src/otel_init.rs +++ b/codex-rs/core/src/otel_init.rs @@ -37,7 +37,7 @@ pub fn build_provider( endpoint: endpoint.clone(), headers: headers .iter() - .map(|(k, v)| (k.clone(), v.clone())) + .map(|(name, value)| (name.clone(), value.as_str().to_owned())) .collect(), protocol, tls: tls.as_ref().map(|config| OtelTlsSettings { @@ -55,7 +55,7 @@ pub fn build_provider( endpoint: endpoint.clone(), headers: headers .iter() - .map(|(k, v)| (k.clone(), v.clone())) + .map(|(name, value)| (name.clone(), value.as_str().to_owned())) .collect(), tls: tls.as_ref().map(|config| OtelTlsSettings { ca_certificate: config.ca_certificate.clone(), diff --git a/codex-rs/utils/redacted-string/src/lib.rs b/codex-rs/utils/redacted-string/src/lib.rs index e0ec95e6e9..0ec6e56130 100644 --- a/codex-rs/utils/redacted-string/src/lib.rs +++ b/codex-rs/utils/redacted-string/src/lib.rs @@ -1,6 +1,7 @@ use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use serde::Serializer; use std::fmt; use std::ops::Deref; use std::ops::DerefMut; @@ -47,3 +48,24 @@ impl fmt::Debug for RedactedString { f.write_str("") } } + +/// An API response string whose serialized form always hides the value. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(transparent)] +pub struct SerdeRedactedString(RedactedString); + +impl SerdeRedactedString { + pub const PLACEHOLDER: &'static str = "[redacted]"; +} + +impl From for SerdeRedactedString { + fn from(value: RedactedString) -> Self { + Self(value) + } +} + +impl Serialize for SerdeRedactedString { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(Self::PLACEHOLDER) + } +} diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 22c937d43f..8234815ec7 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -9157,7 +9157,10 @@ class OtlpHttp(BaseModel): populate_by_name=True, ) endpoint: str - headers: Annotated[dict[str, str], Field(description="Exporter request headers.")] + headers: Annotated[ + dict[str, str], + Field(description="Header names are retained; values are redacted by the server."), + ] protocol: OtelHttpProtocol tls: OtelTlsConfig | None = None @@ -9175,7 +9178,10 @@ class OtlpGrpc(BaseModel): populate_by_name=True, ) endpoint: str - headers: Annotated[dict[str, str], Field(description="Exporter request headers.")] + headers: Annotated[ + dict[str, str], + Field(description="Header names are retained; values are redacted by the server."), + ] tls: OtelTlsConfig | None = None