Sanitize credentials from Git remote metadata (#40713)

## Why

Git remote URLs can embed usernames, passwords, or tokens. Codex carries these
URLs through turn metadata and persisted thread metadata, so credentials must be
removed before a remote enters those paths.

## What changed

- Add `SanitizedGitUrl`, which parses Git URL and SCP-style remotes, strips
  authentication data, and preserves the conventional `git` SSH user.
- Use sanitized remotes when collecting Git metadata, enriching model requests,
  discovering cloud environments, updating thread metadata, and reading legacy
  rollouts. Reject or omit malformed remotes instead of retaining their raw
  contents.
- Keep API, schema, and TypeScript representations as strings while enforcing
  sanitization in Rust.

## Testing

- Cover URL schemes, SCP and IPv6 forms, remote helpers, encoded paths,
  malformed values, and legacy deserialization.
- Verify credentials do not appear in model requests, API responses, SQLite, or
  rollout files.

GitOrigin-RevId: 6435efc4c45bfbfad4723ce7a0457cb00175f25c
This commit is contained in:
Adam Perry @ OpenAI authored and copyberry committed 2026-08-25 21:34:05 +00:00
1 parent 399be2d6b5
commit 1bc02aea50
31 files changed
+1073 -68

No files matched your search

+93
View File
@@ -686,3 +686,96 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e()
Some(false)
);
}
/// Credential-bearing Git remotes must be redacted from both metadata transports.
#[tokio::test]
async fn responses_stream_redacts_git_remote_credentials_from_turn_metadata() {
core_test_support::skip_if_no_network!();
let server = responses::start_mock_server().await;
let test = test_codex()
.with_model("test-gpt-5.1-codex")
.build(&server)
.await
.expect("build test codex");
let cwd = test.cwd_path();
let remote_token = "test-git-secret-token";
let credential_bearing_origin =
"https://alice:test-git-secret-token@example.invalid/openai/codex.git";
for args in [
["init"].as_slice(),
["remote", "add", "origin", credential_bearing_origin].as_slice(),
] {
let output = Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.expect("git command should run");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let first_response = responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call(
"wait-for-git",
"test_sync_tool",
r#"{"wait_for_git_enrichment":true}"#,
),
responses::ev_completed("resp-1"),
]);
let follow_up_response = responses::sse(vec![
responses::ev_response_created("resp-2"),
responses::ev_assistant_message("msg-1", "done"),
responses::ev_completed("resp-2"),
]);
let request_log = responses::mount_response_sequence(
&server,
vec![
responses::sse_response(first_response),
responses::sse_response(follow_up_response),
],
)
.await;
test.submit_turn("inspect the workspace")
.await
.expect("submit turn prompt");
let requests = request_log.requests();
assert_eq!(requests.len(), 2, "expected a follow-up model request");
let header_metadata = requests[1]
.header("x-codex-turn-metadata")
.expect("request header should include turn metadata");
let request_body = requests[1].body_json();
let body_metadata = request_body["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("request body should include turn metadata");
for (location, metadata) in [
("request header", header_metadata.as_str()),
("request body", body_metadata),
] {
assert!(
!metadata.contains(remote_token),
"{location} should not expose git remote credentials"
);
let metadata: serde_json::Value =
serde_json::from_str(metadata).expect("turn metadata should be valid JSON");
let remotes = metadata
.get("workspaces")
.and_then(serde_json::Value::as_object)
.and_then(|workspaces| workspaces.values().next())
.and_then(|workspace| workspace.get("associated_remote_urls"))
.expect("turn metadata should include git remote URLs");
assert_eq!(
remotes,
&serde_json::json!({ "origin": "https://example.invalid/openai/codex.git" }),
"{location} should contain the sanitized remote"
);
}
}