From 82f93a13b2bd918dcd58a6f9e33a8a8a8559fecb Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Tue, 10 Feb 2026 11:50:07 -0800 Subject: [PATCH] include sandbox (seatbelt, elevated, etc.) as in turn metadata header (#10946) This will help us understand retention/usage for folks who use the Windows (or any other) sandboxes --- codex-rs/core/src/codex.rs | 9 ++- codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/sandbox_tags.rs | 24 +++++++ codex-rs/core/src/tools/registry.rs | 21 +----- codex-rs/core/src/turn_metadata.rs | 38 ++++++---- codex-rs/core/tests/responses_headers.rs | 88 ++++++++++++++---------- 6 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 codex-rs/core/src/sandbox_tags.rs diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 509a9d207..c1e5cf642 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -31,6 +31,7 @@ use crate::models_manager::manager::ModelsManager; use crate::parse_command::parse_command; use crate::parse_turn_item; use crate::rollout::session_index; +use crate::sandbox_tags::sandbox_tag; use crate::stream_events_utils::HandleOutputCtx; use crate::stream_events_utils::handle_non_tool_response_item; use crate::stream_events_utils::handle_output_item_done; @@ -582,8 +583,11 @@ impl TurnContext { } async fn build_turn_metadata_header(&self) -> Option { + let sandbox = sandbox_tag(&self.sandbox_policy, self.windows_sandbox_level); self.turn_metadata_header - .get_or_init(|| async { build_turn_metadata_header(self.cwd.clone()).await }) + .get_or_init(|| async { + build_turn_metadata_header(self.cwd.as_path(), Some(sandbox)).await + }) .await .clone() } @@ -1130,8 +1134,9 @@ impl Session { ), }; + let prewarm_cwd = session_configuration.cwd.clone(); let turn_metadata_header = resolve_turn_metadata_header_with_timeout( - build_turn_metadata_header(session_configuration.cwd.clone()), + async move { build_turn_metadata_header(prewarm_cwd.as_path(), None).await }, None, ) .boxed(); diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 3713c8892..1500a7615 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -57,6 +57,7 @@ pub mod path_utils; pub mod personality_migration; pub mod powershell; mod proposed_plan_parser; +mod sandbox_tags; pub mod sandboxing; mod session_prefix; mod stream_events_utils; diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs new file mode 100644 index 000000000..d81647e71 --- /dev/null +++ b/codex-rs/core/src/sandbox_tags.rs @@ -0,0 +1,24 @@ +use crate::exec::SandboxType; +use crate::protocol::SandboxPolicy; +use crate::safety::get_platform_sandbox; +use codex_protocol::config_types::WindowsSandboxLevel; + +pub(crate) fn sandbox_tag( + policy: &SandboxPolicy, + windows_sandbox_level: WindowsSandboxLevel, +) -> &'static str { + if matches!(policy, SandboxPolicy::DangerFullAccess) { + return "none"; + } + if matches!(policy, SandboxPolicy::ExternalSandbox { .. }) { + return "external"; + } + if cfg!(target_os = "windows") && matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) + { + return "windows_elevated"; + } + + get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) + .map(SandboxType::as_metric_tag) + .unwrap_or("none") +} diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 3de041c57..3dca006d3 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -3,15 +3,13 @@ use std::sync::Arc; use std::time::Duration; use crate::client_common::tools::ToolSpec; -use crate::exec::SandboxType; use crate::function_tool::FunctionCallError; use crate::protocol::SandboxPolicy; -use crate::safety::get_platform_sandbox; +use crate::sandbox_tags::sandbox_tag; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; use async_trait::async_trait; -use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ResponseInputItem; use codex_utils_readiness::Readiness; use tracing::warn; @@ -252,23 +250,6 @@ fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &str) -> Stri } } -fn sandbox_tag(policy: &SandboxPolicy, windows_sandbox_level: WindowsSandboxLevel) -> &'static str { - if matches!(policy, SandboxPolicy::DangerFullAccess) { - return "none"; - } - if matches!(policy, SandboxPolicy::ExternalSandbox { .. }) { - return "external"; - } - if cfg!(target_os = "windows") && matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) - { - return "windows_elevated"; - } - - get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) - .map(SandboxType::as_metric_tag) - .unwrap_or("none") -} - fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str { match policy { SandboxPolicy::ReadOnly => "read-only", diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs index 9ea595554..e17a78804 100644 --- a/codex-rs/core/src/turn_metadata.rs +++ b/codex-rs/core/src/turn_metadata.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use std::future::Future; -use std::path::PathBuf; +use std::path::Path; use std::time::Duration; use serde::Serialize; @@ -45,36 +45,44 @@ where #[derive(Serialize)] struct TurnMetadataWorkspace { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] associated_remote_urls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] latest_git_commit_hash: Option, } #[derive(Serialize)] struct TurnMetadata { + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] workspaces: BTreeMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + sandbox: Option, } -pub async fn build_turn_metadata_header(cwd: PathBuf) -> Option { - let cwd = cwd.as_path(); - let repo_root = get_git_repo_root(cwd)?; +pub async fn build_turn_metadata_header(cwd: &Path, sandbox: Option<&str>) -> Option { + let repo_root = get_git_repo_root(cwd); let (latest_git_commit_hash, associated_remote_urls) = tokio::join!( get_head_commit_hash(cwd), get_git_remote_urls_assume_git_repo(cwd) ); - if latest_git_commit_hash.is_none() && associated_remote_urls.is_none() { + if latest_git_commit_hash.is_none() && associated_remote_urls.is_none() && sandbox.is_none() { return None; } let mut workspaces = BTreeMap::new(); - workspaces.insert( - repo_root.to_string_lossy().into_owned(), - TurnMetadataWorkspace { - associated_remote_urls, - latest_git_commit_hash, - }, - ); - serde_json::to_string(&TurnMetadata { workspaces }).ok() + if let Some(repo_root) = repo_root { + workspaces.insert( + repo_root.to_string_lossy().into_owned(), + TurnMetadataWorkspace { + associated_remote_urls, + latest_git_commit_hash, + }, + ); + } + serde_json::to_string(&TurnMetadata { + workspaces, + sandbox: sandbox.map(ToString::to_string), + }) + .ok() } diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index ecd353a20..31703f796 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -126,6 +126,7 @@ async fn responses_stream_includes_subagent_header_on_review() { request.header("x-openai-subagent").as_deref(), Some("review") ); + assert_eq!(request.header("x-codex-sandbox"), None); } #[tokio::test] @@ -366,11 +367,17 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() test.submit_turn("hello") .await .expect("submit first turn prompt"); + let initial_header = first_request + .single_request() + .header("x-codex-turn-metadata") + .expect("x-codex-turn-metadata header should be present"); + let initial_parsed: serde_json::Value = + serde_json::from_str(&initial_header).expect("x-codex-turn-metadata should be valid JSON"); assert_eq!( - first_request - .single_request() - .header("x-codex-turn-metadata"), - None + initial_parsed + .get("sandbox") + .and_then(serde_json::Value::as_str), + Some("none") ); let git_config_global = cwd.join("empty-git-config"); @@ -423,43 +430,48 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() .await .expect("submit post-git turn prompt"); - let maybe_header = request_recorder + let maybe_metadata = request_recorder .single_request() - .header("x-codex-turn-metadata"); - if let Some(header_value) = maybe_header { - let parsed: serde_json::Value = serde_json::from_str(&header_value) - .expect("x-codex-turn-metadata should be valid JSON"); - let workspaces = parsed - .get("workspaces") - .and_then(serde_json::Value::as_object) - .expect("metadata should include workspaces"); - let workspace = workspaces - .values() - .next() - .expect("metadata should include at least one workspace entry"); - - assert_eq!( - workspace - .get("latest_git_commit_hash") - .and_then(serde_json::Value::as_str), - Some(expected_head.as_str()) - ); - assert_eq!( - workspace - .get("associated_remote_urls") + .header("x-codex-turn-metadata") + .and_then(|header_value| { + let parsed: serde_json::Value = serde_json::from_str(&header_value).ok()?; + let workspace = parsed + .get("workspaces") .and_then(serde_json::Value::as_object) - .and_then(|remotes| remotes.get("origin")) - .and_then(serde_json::Value::as_str), - Some(expected_origin.as_str()) - ); - return; - } + .and_then(|workspaces| workspaces.values().next()) + .cloned()?; + Some((parsed, workspace)) + }); + let Some((parsed, workspace)) = maybe_metadata else { + if tokio::time::Instant::now() >= deadline { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + continue; + }; - if tokio::time::Instant::now() >= deadline { - break; - } - tokio::time::sleep(std::time::Duration::from_millis(25)).await; + assert_eq!( + parsed.get("sandbox").and_then(serde_json::Value::as_str), + Some("none") + ); + assert_eq!( + workspace + .get("latest_git_commit_hash") + .and_then(serde_json::Value::as_str), + Some(expected_head.as_str()) + ); + assert_eq!( + workspace + .get("associated_remote_urls") + .and_then(serde_json::Value::as_object) + .and_then(|remotes| remotes.get("origin")) + .and_then(serde_json::Value::as_str), + Some(expected_origin.as_str()) + ); + return; } - panic!("x-codex-turn-metadata was never observed within 5s after git setup"); + panic!( + "x-codex-turn-metadata with git workspace info was never observed within 5s after git setup" + ); }