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:
andrewgu-oai authored and copyberry committed 2026-08-21 19:04:32 +00:00
1 parent 45a3edc02a
commit 79b7606803
36 files changed
+446 -108

No files matched your search

+28 -16
View File
@@ -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(),