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
This commit is contained in:
iceweasel-oai 2026-02-10 11:50:07 -08:00 committed by GitHub
parent 62d0f302fd
commit 82f93a13b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 106 additions and 75 deletions

View file

@ -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<String> {
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();

View file

@ -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;

View file

@ -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")
}

View file

@ -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",

View file

@ -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<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none")]
latest_git_commit_hash: Option<String>,
}
#[derive(Serialize)]
struct TurnMetadata {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
workspaces: BTreeMap<String, TurnMetadataWorkspace>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sandbox: Option<String>,
}
pub async fn build_turn_metadata_header(cwd: PathBuf) -> Option<String> {
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<String> {
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()
}

View file

@ -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"
);
}