mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
## 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
188 lines
4.8 KiB
Rust
188 lines
4.8 KiB
Rust
use crate::events::CodexAcceptedLineFingerprintsEventParams;
|
|
use crate::events::CodexAcceptedLineFingerprintsEventRequest;
|
|
use crate::events::TrackEventRequest;
|
|
use codex_git_utils::canonicalize_git_remote_url;
|
|
use codex_git_utils::get_git_remote_urls_assume_git_repo;
|
|
use sha1::Digest;
|
|
use std::path::Path;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct AcceptedLineCounts {
|
|
pub(crate) accepted_added_lines: u64,
|
|
pub(crate) accepted_deleted_lines: u64,
|
|
}
|
|
|
|
pub(crate) struct AcceptedLineFingerprintEventInput {
|
|
pub(crate) event_type: &'static str,
|
|
pub(crate) turn_id: String,
|
|
pub(crate) thread_id: String,
|
|
pub(crate) product_surface: Option<String>,
|
|
pub(crate) model_slug: Option<String>,
|
|
pub(crate) completed_at: u64,
|
|
pub(crate) repo_hash: Option<String>,
|
|
pub(crate) accepted_added_lines: u64,
|
|
pub(crate) accepted_deleted_lines: u64,
|
|
}
|
|
|
|
pub(crate) fn accepted_line_counts_from_unified_diff(unified_diff: &str) -> AcceptedLineCounts {
|
|
let mut in_hunk = false;
|
|
let mut accepted_added_lines = 0;
|
|
let mut accepted_deleted_lines = 0;
|
|
|
|
for line in unified_diff.lines() {
|
|
if line.starts_with("diff --git ") {
|
|
in_hunk = false;
|
|
continue;
|
|
}
|
|
|
|
if line.starts_with("@@ ") {
|
|
in_hunk = true;
|
|
continue;
|
|
}
|
|
|
|
if !in_hunk && (line.starts_with("+++ ") || line.starts_with("--- ")) {
|
|
continue;
|
|
}
|
|
|
|
if line.starts_with('+') {
|
|
accepted_added_lines += 1;
|
|
continue;
|
|
}
|
|
|
|
if line.starts_with('-') {
|
|
accepted_deleted_lines += 1;
|
|
}
|
|
}
|
|
|
|
AcceptedLineCounts {
|
|
accepted_added_lines,
|
|
accepted_deleted_lines,
|
|
}
|
|
}
|
|
|
|
pub fn fingerprint_hash(domain: &str, value: &str) -> String {
|
|
let mut hasher = sha1::Sha1::new();
|
|
hasher.update(b"file-line-v1\0");
|
|
hasher.update(domain.as_bytes());
|
|
hasher.update(b"\0");
|
|
hasher.update(value.as_bytes());
|
|
format!("{:x}", hasher.finalize())
|
|
}
|
|
|
|
pub(crate) fn accepted_line_fingerprint_event_requests(
|
|
input: AcceptedLineFingerprintEventInput,
|
|
) -> Vec<TrackEventRequest> {
|
|
let AcceptedLineFingerprintEventInput {
|
|
event_type,
|
|
turn_id,
|
|
thread_id,
|
|
product_surface,
|
|
model_slug,
|
|
completed_at,
|
|
repo_hash,
|
|
accepted_added_lines,
|
|
accepted_deleted_lines,
|
|
} = input;
|
|
|
|
vec![TrackEventRequest::AcceptedLineFingerprints(Box::new(
|
|
CodexAcceptedLineFingerprintsEventRequest {
|
|
event_type: "codex_accepted_line_fingerprints",
|
|
event_params: CodexAcceptedLineFingerprintsEventParams {
|
|
event_type,
|
|
turn_id,
|
|
thread_id,
|
|
product_surface,
|
|
model_slug,
|
|
completed_at,
|
|
repo_hash,
|
|
accepted_added_lines,
|
|
accepted_deleted_lines,
|
|
line_fingerprints: [],
|
|
},
|
|
},
|
|
))]
|
|
}
|
|
|
|
pub async fn accepted_line_repo_hash_for_cwd(cwd: &Path) -> Option<String> {
|
|
let remotes = get_git_remote_urls_assume_git_repo(cwd).await?;
|
|
remotes
|
|
.get("origin")
|
|
.or_else(|| remotes.values().next())
|
|
.map(|remote_url| {
|
|
let canonical_remote_url = canonicalize_git_remote_url(remote_url.as_str())
|
|
.unwrap_or_else(|| remote_url.to_string());
|
|
fingerprint_hash("repo", &canonical_remote_url)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_accepted_line_counts() {
|
|
let diff = "\
|
|
diff --git a/src/lib.rs b/src/lib.rs
|
|
index 1111111..2222222
|
|
--- a/src/lib.rs
|
|
+++ b/src/lib.rs
|
|
@@ -1,3 +1,5 @@
|
|
-old line
|
|
+fn useful() {
|
|
+}
|
|
+ return user.id;
|
|
context
|
|
";
|
|
|
|
assert_eq!(
|
|
accepted_line_counts_from_unified_diff(diff),
|
|
AcceptedLineCounts {
|
|
accepted_added_lines: 3,
|
|
accepted_deleted_lines: 1,
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn skips_added_file_metadata_headers() {
|
|
let diff = "\
|
|
diff --git a/new.py b/new.py
|
|
new file mode 100644
|
|
index 0000000..1111111
|
|
--- /dev/null
|
|
+++ b/new.py
|
|
@@ -0,0 +1 @@
|
|
+print('hello')
|
|
";
|
|
|
|
assert_eq!(
|
|
accepted_line_counts_from_unified_diff(diff),
|
|
AcceptedLineCounts {
|
|
accepted_added_lines: 1,
|
|
accepted_deleted_lines: 0,
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_hunk_lines_that_look_like_file_headers() {
|
|
let diff = "\
|
|
diff --git a/src/lib.rs b/src/lib.rs
|
|
index 1111111..2222222
|
|
--- a/src/lib.rs
|
|
+++ b/src/lib.rs
|
|
@@ -1,2 +1,2 @@
|
|
--- old value
|
|
+++ new value
|
|
";
|
|
|
|
assert_eq!(
|
|
accepted_line_counts_from_unified_diff(diff),
|
|
AcceptedLineCounts {
|
|
accepted_added_lines: 1,
|
|
accepted_deleted_lines: 1,
|
|
},
|
|
);
|
|
}
|
|
}
|