Encrypt sensitive history and notes tool arguments (#41041)

## What changed

- Mark history and notes search queries, appended note text, and replacement note text as encrypted in their tool schemas.
- Send `x-openai-encrypted-tool-arguments: true` for the corresponding backend routes without changing the JSON request body.

## Testing

- Cover encrypted schema fields and backend headers for history search, notes search, append, and write calls.
- Verify unrelated history and notes requests do not receive the encrypted-arguments header.

GitOrigin-RevId: 58263cff832c979b436ddb4013b5c27218aca678
This commit is contained in:
pmccrary-oai authored and copyberry committed 2026-08-27 03:59:54 +00:00
1 parent 5af6979986
commit 57e2edc6e9
4 files changed
+100 -9

No files matched your search

+14
View File
@@ -5,11 +5,13 @@ use codex_client::HttpTransport;
use codex_client::RequestBody;
use codex_login::default_client::create_client;
use codex_model_provider::SharedModelProvider;
use http::HeaderValue;
use http::Method;
use serde_json::Value;
use serde_json::json;
const HISTORY_NOTES_BACKEND_TIMEOUT: Duration = Duration::from_secs(35);
const ENCRYPTED_TOOL_ARGUMENTS_HEADER: &str = "x-openai-encrypted-tool-arguments";
#[derive(Clone)]
pub(crate) struct HistoryNotesBackend {
@@ -50,6 +52,18 @@ impl HistoryNotesBackend {
.map_err(|error| format!("History backend auth could not be resolved: {error}"))?;
let mut request = provider.build_request(Method::POST, path);
if matches!(
path,
"alpha/history/v2/search_contents"
| "alpha/notes/v2/search_contents"
| "alpha/notes/v2/append_to_file"
| "alpha/notes/v2/write_file"
) {
request.headers.insert(
ENCRYPTED_TOOL_ARGUMENTS_HEADER,
HeaderValue::from_static("true"),
);
}
request.body = Some(RequestBody::Json(arguments));
request.timeout = Some(HISTORY_NOTES_BACKEND_TIMEOUT);
let request = auth
@@ -14,6 +14,7 @@ use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
use super::ENCRYPTED_TOOL_ARGUMENTS_HEADER;
use super::HistoryNotesBackend;
#[tokio::test]
@@ -62,6 +63,12 @@ async fn routes_through_codex_backend_and_injects_trusted_session_agent_context(
assert_eq!(response, json!({"encrypted_output": "enc_payload"}));
let requests = server.received_requests().await.expect("recorded requests");
assert_eq!(requests.len(), 1);
assert!(
requests[0]
.headers
.get(ENCRYPTED_TOOL_ARGUMENTS_HEADER)
.is_none()
);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&requests[0].body).expect("JSON body"),
json!({
@@ -73,3 +80,57 @@ async fn routes_through_codex_backend_and_injects_trusted_session_agent_context(
})
);
}
#[tokio::test]
async fn marks_encrypted_history_and_notes_arguments_without_changing_the_json_body() {
let server = MockServer::start().await;
let cases = [
(
"history/v2/search_contents",
json!({"query": "encrypted-query"}),
),
(
"notes/v2/search_contents",
json!({"query": "encrypted-query"}),
),
(
"notes/v2/append_to_file",
json!({"path": "notes.md", "text": "encrypted-text"}),
),
(
"notes/v2/write_file",
json!({"path": "notes.md", "text": "encrypted-text"}),
),
];
for (route, _) in &cases {
Mock::given(method("POST"))
.and(path(format!("/backend-api/codex/alpha/{route}")))
.and(header(ENCRYPTED_TOOL_ARGUMENTS_HEADER, "true"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"ok": true})))
.expect(1)
.mount(&server)
.await;
}
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::Headers(AuthHeaders::new(HeaderMap::new())));
let backend = HistoryNotesBackend::new(create_model_provider(
ModelProviderInfo::create_openai_provider(Some(format!(
"{}/backend-api/codex",
server.uri()
))),
Some(auth_manager),
));
for (route, arguments) in cases {
backend
.call(
&format!("alpha/{route}"),
"session-123",
"/root",
arguments.clone(),
)
.await
.expect("encrypted argument request should succeed");
}
}
+4 -4
View File
@@ -179,7 +179,7 @@ impl HistoryNotesAction {
"type": "object",
"properties": {
"limit": {"type": "integer", "minimum": 1, "description": "Maximum number of matching items to return."},
"query": {"type": "string", "description": "Case-sensitive literal substring to find in item content."},
"query": {"type": "string", "encrypted": true, "description": "Case-sensitive literal substring to find in item content."},
"recent_first": {"type": "boolean", "description": "Whether to return the most recently created matches first."},
"tool_namespace": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Callable namespace to include. When set, non-tool messages are excluded."},
"role": {"anyOf": [{"type": "string", "enum": ["user", "assistant", "tool", "system", "developer"]}, {"type": "null"}], "description": "Message role to include. Null or omission includes all roles."},
@@ -211,7 +211,7 @@ impl HistoryNotesAction {
"type": "object",
"properties": {
"max_matches_per_file": {"type": "integer", "minimum": 1, "description": "Maximum number of matching lines returned per file."},
"query": {"type": "string", "description": "Case-sensitive literal substring to find in note lines."},
"query": {"type": "string", "encrypted": true, "description": "Case-sensitive literal substring to find in note lines."},
"recent_file_first": {"type": "boolean", "description": "Whether to order matching files by creation time, newest first."},
"max_files": {"type": "integer", "minimum": 1, "description": "Maximum number of matching files returned."},
"path_prefix": {"anyOf": [{"type": "string"}, {"type": "null"}], "description": "Note path prefix to search."}
@@ -221,7 +221,7 @@ impl HistoryNotesAction {
Self::NotesAppendToFile => json!({
"type": "object",
"properties": {
"text": {"type": "string", "description": "Text appended exactly as provided."},
"text": {"type": "string", "encrypted": true, "description": "Text appended exactly as provided."},
"path": {"type": "string", "description": "Note file path to append to."}
},
"required": ["text", "path"]
@@ -229,7 +229,7 @@ impl HistoryNotesAction {
Self::NotesWriteFile => json!({
"type": "object",
"properties": {
"text": {"type": "string", "description": "Complete replacement text for the file."},
"text": {"type": "string", "encrypted": true, "description": "Complete replacement text for the file."},
"path": {"type": "string", "description": "Note file path to create or replace."}
},
"required": ["text", "path"]