feat(codex): align Search and execution protocol

This commit is contained in:
MMEXA
2026-07-12 03:04:15 +08:00
parent 59d37ae1dd
commit 2316df5c9a
121 changed files with 5141 additions and 543 deletions
+180 -1
View File
@@ -1063,6 +1063,7 @@ impl MaskChatRequestOptions {
pub(crate) enum ChatPiiRedactionRequestFormat {
OpenAiChat,
OpenAiResponses,
OpenAiSearch,
ClaudeMessages,
}
@@ -1071,6 +1072,7 @@ impl ChatPiiRedactionRequestFormat {
match api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => Some(Self::OpenAiChat),
"openai:responses" | "openai:responses:compact" => Some(Self::OpenAiResponses),
"openai:search" => Some(Self::OpenAiSearch),
"claude:messages" => Some(Self::ClaudeMessages),
_ => None,
}
@@ -1604,6 +1606,7 @@ fn request_collision_corpus(format: ChatPiiRedactionRequestFormat, value: &Value
.map(|messages| chat_message_collision_corpus(messages))
.unwrap_or_default(),
ChatPiiRedactionRequestFormat::OpenAiResponses => openai_responses_collision_corpus(value),
ChatPiiRedactionRequestFormat::OpenAiSearch => openai_search_collision_corpus(value),
ChatPiiRedactionRequestFormat::ClaudeMessages => claude_messages_collision_corpus(value),
}
}
@@ -1622,6 +1625,9 @@ fn mask_request_value(
ChatPiiRedactionRequestFormat::OpenAiResponses => {
mask_openai_responses_request_value(value, session, scan_state)
}
ChatPiiRedactionRequestFormat::OpenAiSearch => {
mask_openai_search_request_value(value, session, scan_state)
}
ChatPiiRedactionRequestFormat::ClaudeMessages => {
mask_claude_messages_request_value(value, session, scan_state)
}
@@ -1643,6 +1649,9 @@ async fn mask_request_value_async(
ChatPiiRedactionRequestFormat::OpenAiResponses => {
mask_openai_responses_request_value_async(value, session, scan_state, cache).await
}
ChatPiiRedactionRequestFormat::OpenAiSearch => {
mask_openai_search_request_value_async(value, session, scan_state, cache).await
}
ChatPiiRedactionRequestFormat::ClaudeMessages => {
mask_claude_messages_request_value_async(value, session, scan_state, cache).await
}
@@ -1810,6 +1819,31 @@ fn openai_responses_collision_corpus(value: &Value) -> Vec<String> {
corpus
}
const OPENAI_SEARCH_COMMAND_TEXT_FIELDS: [(&str, &str); 4] = [
("search_query", "q"),
("image_query", "q"),
("find", "pattern"),
("weather", "location"),
];
fn openai_search_collision_corpus(value: &Value) -> Vec<String> {
let mut corpus = openai_responses_collision_corpus(value);
let Some(commands) = value.get("commands").and_then(Value::as_object) else {
return corpus;
};
for (command, field) in OPENAI_SEARCH_COMMAND_TEXT_FIELDS {
let Some(entries) = commands.get(command).and_then(Value::as_array) else {
continue;
};
for entry in entries {
if let Some(text) = entry.get(field).and_then(Value::as_str) {
corpus.push(text.to_string());
}
}
}
corpus
}
fn collect_openai_responses_input_collision_text(value: &Value, corpus: &mut Vec<String>) {
match value {
Value::String(text) => corpus.push(text.clone()),
@@ -2025,6 +2059,31 @@ fn mask_openai_responses_request_value(
Ok(redacted)
}
fn mask_openai_search_request_value(
value: &mut Value,
session: &mut RedactionSession,
scan_state: &mut RedactionScanState,
) -> Result<bool, RedactionLimitError> {
let mut redacted = false;
if let Some(input) = value.get_mut("input") {
redacted |= mask_openai_responses_input_value(input, session, scan_state)?;
}
let Some(commands) = value.get_mut("commands").and_then(Value::as_object_mut) else {
return Ok(redacted);
};
for (command, field) in OPENAI_SEARCH_COMMAND_TEXT_FIELDS {
let Some(entries) = commands.get_mut(command).and_then(Value::as_array_mut) else {
continue;
};
for entry in entries {
if let Some(Value::String(text)) = entry.get_mut(field) {
redacted |= mask_json_string(text, session, scan_state)?;
}
}
}
Ok(redacted)
}
fn mask_openai_responses_input_value(
value: &mut Value,
session: &mut RedactionSession,
@@ -2310,6 +2369,33 @@ async fn mask_openai_responses_request_value_async(
Ok(redacted)
}
async fn mask_openai_search_request_value_async(
value: &mut Value,
session: &mut RedactionSession,
scan_state: &mut RedactionScanState,
cache: Option<&RedisRedactionMappingCache<'_>>,
) -> Result<bool, RedactionMaskError> {
let mut redacted = false;
if let Some(input) = value.get_mut("input") {
redacted |=
mask_openai_responses_input_value_async(input, session, scan_state, cache).await?;
}
let Some(commands) = value.get_mut("commands").and_then(Value::as_object_mut) else {
return Ok(redacted);
};
for (command, field) in OPENAI_SEARCH_COMMAND_TEXT_FIELDS {
let Some(entries) = commands.get_mut(command).and_then(Value::as_array_mut) else {
continue;
};
for entry in entries {
if let Some(Value::String(text)) = entry.get_mut(field) {
redacted |= mask_json_string_async(text, session, scan_state, cache).await?;
}
}
}
Ok(redacted)
}
async fn mask_openai_responses_input_value_async(
value: &mut Value,
session: &mut RedactionSession,
@@ -4072,6 +4158,7 @@ mod tests {
build_redaction_session_config, detect_candidates_with_probe, mask_chat_request_json,
mask_chat_request_json_with_options, parse_chat_pii_redaction_rules,
restore_sync_response_body, try_mask_chat_pii_request_json_with_options,
try_mask_chat_pii_request_value_with_cache_options,
try_mask_chat_request_json_with_cache_options, try_mask_chat_request_json_with_options,
ChatPiiRedactionRequestFormat, ChatPiiRedactionRuntimeConfig, DetectorProbe, MappingKey,
MaskChatRequestOptions, RedactionKind, RedactionLimitError, RedactionMapping,
@@ -4083,7 +4170,7 @@ mod tests {
use aether_runtime_state::{RedisClientConfig, RuntimeState};
use aether_testkit::ManagedRedisServer;
use serde_json::json;
use serde_json::{json, Value};
fn assert_debug_surface_hides_values(debug: &str, originals: &[&str], sentinels: &[String]) {
for original in originals {
@@ -4734,6 +4821,98 @@ mod tests {
.contains("secretValueABCDEF1234567890abcdef"));
}
#[test]
fn pii_redaction_request_masks_openai_search_text_fields() {
let request = json!({
"id": "session-1",
"model": "gpt-5.6",
"input": "Find [email protected]",
"commands": {
"search_query": [{"q": "Phone +14155552671"}],
"image_query": [{"q": "Image for [email protected]"}],
"find": [{"ref_id": "https://example.com/[email protected]", "pattern": "secret_key=secretValueABCDEF1234567890abcdef"}],
"weather": [{"location": "Contact [email protected]"}],
"open": [{"ref_id": "https://example.com/[email protected]"}]
}
});
let raw = serde_json::to_vec(&request).expect("request should serialize");
let masked = try_mask_chat_pii_request_json_with_options(
&raw,
ChatPiiRedactionRequestFormat::OpenAiSearch,
test_config(),
MaskChatRequestOptions::runtime(),
)
.expect("search request should mask");
let masked_json: Value =
serde_json::from_slice(&masked.body).expect("masked request should parse");
assert!(masked.redacted);
assert!(!masked_json["input"]
.as_str()
.unwrap()
.contains("[email protected]"));
assert!(!masked_json["commands"]["search_query"][0]["q"]
.as_str()
.unwrap()
.contains("+14155552671"));
assert!(!masked_json["commands"]["image_query"][0]["q"]
.as_str()
.unwrap()
.contains("[email protected]"));
assert!(!masked_json["commands"]["find"][0]["pattern"]
.as_str()
.unwrap()
.contains("secretValueABCDEF1234567890abcdef"));
assert!(!masked_json["commands"]["weather"][0]["location"]
.as_str()
.unwrap()
.contains("[email protected]"));
assert_eq!(
masked_json["commands"]["open"][0]["ref_id"],
"https://example.com/[email protected]"
);
}
#[tokio::test]
async fn pii_redaction_async_request_masks_openai_search_text_fields() {
let request = json!({
"id": "session-1",
"model": "gpt-5.6",
"input": "Find [email protected]",
"commands": {
"search_query": [{"q": "Phone +14155552671"}],
"find": [{"ref_id": "turn0search0", "pattern": "[email protected]"}]
}
});
let masked = try_mask_chat_pii_request_value_with_cache_options(
&request,
ChatPiiRedactionRequestFormat::OpenAiSearch,
test_config(),
MaskChatRequestOptions::runtime(),
None,
)
.await
.expect("search request should mask");
let masked_json = masked.body_json.expect("masked body should be present");
assert!(masked.redacted);
assert!(!masked_json["input"]
.as_str()
.unwrap()
.contains("[email protected]"));
assert!(!masked_json["commands"]["search_query"][0]["q"]
.as_str()
.unwrap()
.contains("+14155552671"));
assert!(!masked_json["commands"]["find"][0]["pattern"]
.as_str()
.unwrap()
.contains("[email protected]"));
assert_eq!(masked_json["commands"]["find"][0]["ref_id"], "turn0search0");
}
#[test]
fn pii_redaction_request_avoids_cross_message_and_tool_argument_sentinel_collisions() {
let mut probe = session_at(600);