mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
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:
1 parent
5af6979986
commit
57e2edc6e9
4 files changed
+100
-9
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
Reference in new issue
Block a user