Include sandbox mode in response metadata (#37507)

## What changed

- Add the effective permission profile as `sandbox_mode` in turn metadata for regular, prewarm, compaction, and detached memory requests.
- Reserve `sandbox_mode` so client-provided metadata cannot override the computed value.

## Testing

- Cover metadata headers and request bodies for read-only and danger-full-access modes.

GitOrigin-RevId: 13c690cc99bc2691023f281ca527af049bb1286f
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-07 22:41:20 +00:00
1 parent 45f8cafa4e
commit 4ca25a2c4e
10 files changed
+124 -15

No files matched your search

@@ -600,6 +600,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
compact_metadata["request_kind"].as_str(),
Some("compaction")
);
assert_eq!(compact_metadata["sandbox_mode"].as_str(), Some("read-only"));
assert_eq!(
compact_metadata["window_id"].as_str(),
compact_request.header("x-codex-window-id").as_deref()
@@ -11,7 +11,7 @@ Scenario: After five varied API-key-auth turns, remote manual compaction omits s
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"sandbox\":\"<SANDBOX>\",\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
@@ -11,7 +11,7 @@ Scenario: After five varied ChatGPT-auth turns, remote manual compaction reuses
- "thread_id": "<UUID>",
- "turn_id": "<UUID>",
- "x-codex-installation-id": "<UUID>",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"sandbox\":\"<SANDBOX>\",\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-turn-metadata": "{\"installation_id\":\"<UUID>\",\"session_id\":\"<UUID>\",\"thread_id\":\"<UUID>\",\"turn_id\":\"<UUID>\",\"window_id\":\"<UUID>:0\",\"request_kind\":\"turn\",\"sandbox\":\"<SANDBOX>\",\"sandbox_mode\":\"read-only\",\"turn_started_at_unix_ms\":<UNIX_MS>}",
- "x-codex-window-id": "<UUID>:0"
- },
- "include": [
+62 -9
View File
@@ -94,6 +94,7 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
let server = start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
requests: vec![
vec![ev_response_created("warm-1"), ev_completed("warm-1")],
vec![
json!({
"type": "response.metadata",
@@ -110,9 +111,9 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
ev_completed("resp-2"),
],
vec![
ev_response_created("resp-3"),
ev_response_created("resp-4"),
ev_assistant_message("msg-2", "done"),
ev_completed("resp-3"),
ev_completed("resp-4"),
],
],
response_headers: Vec::new(),
@@ -123,22 +124,74 @@ async fn websocket_turn_state_persists_within_turn_and_resets_after() -> Result<
let mut builder = test_codex();
let test = builder.build_with_websocket_server(&server).await?;
// Phase 1: the first response mints state for its same-turn tool follow-up.
// Phase 1: startup prewarm uses the connection without generating a response.
// Phase 2: the first turn mints state for its same-turn tool follow-up.
test.submit_turn("run the echo command").await?;
// Phase 2: the follow-up replays that state on the same physical connection.
// Phase 3: the next logical turn reuses the connection but starts with empty state.
// Phase 3: the follow-up replays that state on the same physical connection.
// Phase 4: the next logical turn reuses the connection but starts with empty state.
test.submit_turn("start another turn").await?;
assert_eq!(server.handshakes().len(), 1);
let requests = server.single_connection();
assert_eq!(requests.len(), 3);
assert_eq!(requests.len(), 4);
let bodies = requests
.iter()
.map(core_test_support::responses::WebSocketRequest::body_json)
.collect::<Vec<_>>();
assert_eq!(
requests
bodies
.iter()
.map(|request| request.body_json()["client_metadata"][TURN_STATE_HEADER].clone())
.map(|body| body["client_metadata"][TURN_STATE_HEADER].clone())
.collect::<Vec<_>>(),
vec![json!(null), json!("ts-1"), json!(null)]
vec![json!(null), json!(null), json!("ts-1"), json!(null)]
);
let turn_metadata = bodies
.iter()
.map(|body| {
serde_json::from_str::<Value>(
body["client_metadata"]["x-codex-turn-metadata"]
.as_str()
.expect("websocket request should include turn metadata"),
)
})
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(
turn_metadata
.iter()
.map(|metadata| metadata["request_kind"].clone())
.collect::<Vec<_>>(),
vec![
json!("prewarm"),
json!("turn"),
json!("turn"),
json!("turn"),
]
);
assert_eq!(bodies[0]["generate"], json!(false));
assert!(bodies[1..].iter().all(|body| body["generate"].is_null()));
assert_eq!(
turn_metadata
.iter()
.map(|metadata| metadata["sandbox_mode"].clone())
.collect::<Vec<_>>(),
vec![
json!("read-only"),
json!("danger-full-access"),
json!("danger-full-access"),
json!("danger-full-access"),
]
);
let first_turn_id = turn_metadata[1]["turn_id"]
.as_str()
.expect("first turn should include turn_id");
let follow_up_turn_id = turn_metadata[2]["turn_id"]
.as_str()
.expect("same-turn follow-up should include turn_id");
let next_turn_id = turn_metadata[3]["turn_id"]
.as_str()
.expect("next turn should include turn_id");
assert_eq!(first_turn_id, follow_up_turn_id);
assert_ne!(follow_up_turn_id, next_turn_id);
server.shutdown().await;
Ok(())