Record sandbox backends in command execution analytics (#47568)

## Why

Command execution analytics lack the observed sandbox backend. Older execution servers may omit this information, so an unknown backend must remain distinguishable from an explicitly unsandboxed process.

## What changed

- Carry optional `sandbox_type` metadata through command execution items and emit its metric tag as `sandbox_backend` in analytics.
- Propagate the observed backend through unified execution completion and failure events, including asynchronous exits.
- Preserve missing backend information as `None` and mark executed user shell commands as `Some(SandboxType::None)`.
- Exclude the metadata from serialized command items and generated schemas and TypeScript types.

## Testing

Extend tests for missing and explicit executor backends, backend propagation on failed execution, and analytics payloads, including platform-specific backend tags in app-server integration coverage.

GitOrigin-RevId: 178fe46824a6194910802ec02168ed40ee0b2019
This commit is contained in:
iceweasel-oai
2026-09-23 15:05:59 +00:00
committed by copyberry
parent d6c4c6aea4
commit 6f6af0fce4
29 changed files with 113 additions and 21 deletions
+1
View File
@@ -793,6 +793,7 @@ pub(crate) enum WebSearchActionKind {
#[derive(Serialize)]
pub(crate) struct CodexCommandExecutionEventParams {
pub(crate) sandbox_backend: Option<String>,
pub(crate) model_slug: Option<String>,
pub(crate) reasoning_effort: Option<String>,
#[serde(flatten)]
+3
View File
@@ -2772,6 +2772,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
match item {
ThreadItem::CommandExecution {
id,
sandbox_type,
plugin_id,
script_path,
source,
@@ -2806,6 +2807,8 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
CodexCommandExecutionEventRequest {
event_type: "codex_command_execution_event",
event_params: CodexCommandExecutionEventParams {
sandbox_backend: sandbox_type
.map(|sandbox| sandbox.as_metric_tag().to_string()),
model_slug: model_context.map(|context| context.model_slug.clone()),
reasoning_effort: model_context
.and_then(|context| context.reasoning_effort.clone()),
@@ -49,6 +49,7 @@ use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadItem;
use codex_app_server_protocol::TurnStatus as AppServerTurnStatus;
use codex_protocol::protocol::ThreadSource;
use codex_protocol::sandbox::SandboxType;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use pretty_assertions::assert_eq;
@@ -61,12 +62,14 @@ fn sample_command_execution_item_with_actions(
command_actions: Vec<CommandAction>,
plugin_id: Option<&str>,
script_path: Option<&str>,
sandbox_type: Option<SandboxType>,
) -> ThreadItem {
let mut item = sample_command_execution_item(status, exit_code, duration_ms);
let ThreadItem::CommandExecution {
command_actions: item_command_actions,
plugin_id: item_plugin_id,
script_path: item_script_path,
sandbox_type: item_sandbox_type,
..
} = &mut item
else {
@@ -75,6 +78,7 @@ fn sample_command_execution_item_with_actions(
*item_command_actions = command_actions;
*item_plugin_id = plugin_id.map(str::to_string);
*item_script_path = script_path.map(str::to_string);
*item_sandbox_type = sandbox_type;
item
}
@@ -96,6 +100,7 @@ fn command_execution_event_serializes_expected_shape() {
let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest {
event_type: "codex_command_execution_event",
event_params: CodexCommandExecutionEventParams {
sandbox_backend: None,
model_slug: None,
reasoning_effort: None,
base: CodexToolItemEventBase {
@@ -155,6 +160,7 @@ fn command_execution_event_serializes_expected_shape() {
let mut expected = json!({
"event_type": "codex_command_execution_event",
"event_params": {
"sandbox_backend": null,
"model_slug": null,
"reasoning_effort": null,
"thread_id": "thread-1",
@@ -291,6 +297,7 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
],
Some("sample@openai-curated"),
Some("scripts/run.py"),
Some(SandboxType::WindowsMxc),
),
},
))),
@@ -301,6 +308,7 @@ async fn item_lifecycle_notifications_publish_command_execution_event() {
let payload = serde_json::to_value(&events).expect("serialize events");
assert_eq!(payload.as_array().expect("events array").len(), 1);
assert_eq!(payload[0]["event_params"]["model_slug"], "invoking-model");
assert_eq!(payload[0]["event_params"]["sandbox_backend"], "windows_mxc");
assert_eq!(payload[0]["event_params"]["reasoning_effort"], "max");
assert_eq!(payload[0]["event_type"], "codex_command_execution_event");
assert_eq!(payload[0]["event_params"]["thread_id"], "thread-1");
+1
View File
@@ -647,6 +647,7 @@ pub(super) fn sample_command_execution_item_with_id(
) -> ThreadItem {
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: id.to_string(),
plugin_id: None,
script_path: None,
@@ -98,6 +98,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
CommandExecutionPresentation::from_raw(&payload.command, &payload.parsed_cmd, &payload.cwd);
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: payload.call_id.clone(),
plugin_id: payload.plugin_id.clone(),
script_path: payload.script_path.clone(),
@@ -125,6 +126,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: payload.call_id.clone(),
plugin_id: payload.plugin_id.clone(),
script_path: payload.script_path.clone(),
@@ -202,6 +204,7 @@ pub fn build_item_from_guardian_event(
Some(ThreadItem::CommandExecution {
id: id.clone(),
model_context: assessment.model_context.clone(),
sandbox_type: None,
plugin_id: assessment.plugin_id.clone(),
script_path: assessment.script_path.clone(),
command,
@@ -241,6 +244,7 @@ pub fn build_item_from_guardian_event(
Some(ThreadItem::CommandExecution {
id: id.clone(),
model_context: assessment.model_context.clone(),
sandbox_type: None,
plugin_id: assessment.plugin_id.clone(),
script_path: assessment.script_path.clone(),
command,
@@ -2391,6 +2391,7 @@ mod tests {
}];
let command_item = CoreTurnItem::CommandExecution(CoreCommandExecutionItem {
model_context: None,
sandbox_type: None,
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
@@ -2477,6 +2478,7 @@ mod tests {
build_turns_from_rollout_items(&items[..2])[0].items,
vec![ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
@@ -2503,6 +2505,7 @@ mod tests {
turns[0].items,
vec![ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
@@ -3191,6 +3194,7 @@ mod tests {
turns[0].items[2],
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-1".into(),
plugin_id: None,
script_path: None,
@@ -3465,6 +3469,7 @@ mod tests {
turns[0].items[1],
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-declined".into(),
plugin_id: None,
script_path: None,
@@ -3575,6 +3580,7 @@ mod tests {
turns[0].items[1],
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "guardian-exec".into(),
plugin_id: Some("sample@openai-curated".into()),
script_path: Some("scripts/run.py".into()),
@@ -3649,6 +3655,7 @@ mod tests {
turns[0].items[1],
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "guardian-execve".into(),
plugin_id: Some("sample@openai-curated".into()),
script_path: Some("scripts/run.py".into()),
@@ -3852,6 +3859,7 @@ mod tests {
turns[0].items[1],
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-late".into(),
plugin_id: None,
script_path: None,
@@ -289,6 +289,10 @@ pub enum ThreadItem {
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
CommandExecution {
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
@@ -919,6 +923,7 @@ impl From<CoreTurnItem> for ThreadItem {
ThreadItem::CommandExecution {
id: command.id,
model_context: command.model_context,
sandbox_type: command.sandbox_type,
plugin_id: command.plugin_id,
script_path: command.script_path,
command: presentation.command,
@@ -3272,6 +3272,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
let command_item = TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
@@ -3305,6 +3306,7 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
ThreadItem::from(command_item),
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "exec-1".to_string(),
plugin_id: Some("sample@openai-curated".to_string()),
script_path: Some("scripts/run.py".to_string()),
@@ -1422,6 +1422,7 @@ async fn start_command_execution_item(
item: ThreadItem::CommandExecution {
id: item_id,
model_context,
sandbox_type: None,
plugin_id,
script_path,
command,
@@ -1467,6 +1468,7 @@ async fn complete_command_execution_item(
let item = ThreadItem::CommandExecution {
id: item_id,
model_context: completion_item.model_context,
sandbox_type: None,
plugin_id: completion_item.plugin_id,
script_path: completion_item.script_path,
command: completion_item.command,
@@ -2484,6 +2486,7 @@ mod tests {
payload.item,
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
plugin_id: completion_item.plugin_id.clone(),
script_path: completion_item.script_path.clone(),
@@ -963,6 +963,7 @@ enabled = true
assert_eq!(
json!({
"model_slug": command_event["event_params"]["model_slug"],
"sandbox_backend": command_event["event_params"]["sandbox_backend"],
"reasoning_effort": command_event["event_params"]["reasoning_effort"],
"plugin_id": command_event["event_params"]["plugin_id"],
"script_path": command_event["event_params"]["script_path"],
@@ -971,6 +972,7 @@ enabled = true
}),
json!({
"model_slug": "invoking-model",
"sandbox_backend": if cfg!(target_os = "macos") { "seatbelt" } else { "seccomp" },
"reasoning_effort": "high",
"plugin_id": METRICS_PLUGIN_ID,
"script_path": "scripts/run.sh",
+4
View File
@@ -189,6 +189,7 @@ pub(crate) async fn execute_user_shell_command(
turn_context.as_ref(),
&TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: call_id.clone(),
plugin_id: None,
script_path: None,
@@ -269,6 +270,7 @@ pub(crate) async fn execute_user_shell_command(
turn_context.as_ref(),
TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: call_id,
plugin_id: None,
script_path: None,
@@ -295,6 +297,7 @@ pub(crate) async fn execute_user_shell_command(
turn_context.as_ref(),
TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: Some(SandboxType::None),
id: call_id.clone(),
plugin_id: None,
script_path: None,
@@ -341,6 +344,7 @@ pub(crate) async fn execute_user_shell_command(
turn_context.as_ref(),
TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: call_id,
plugin_id: None,
script_path: None,
+4
View File
@@ -44,6 +44,7 @@ pub(super) fn truncate_rejection_message(message: &str) -> String {
#[derive(Clone, Copy)]
pub(crate) struct ToolEventCtx<'a> {
pub sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
pub model_context: Option<&'a ModelInvocationContext>,
pub session: &'a Session,
pub turn: &'a TurnContext,
@@ -63,6 +64,7 @@ impl<'a> ToolEventCtx<'a> {
) -> Self {
Self {
model_context: None,
sandbox_type: None,
session,
turn,
model_info,
@@ -173,6 +175,7 @@ async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommand
&TurnItem::CommandExecution(CommandExecutionItem {
id: ctx.call_id.to_string(),
model_context: ctx.model_context.cloned(),
sandbox_type: ctx.sandbox_type,
plugin_id,
script_path,
process_id: exec_input.process_id.map(str::to_owned),
@@ -587,6 +590,7 @@ async fn emit_exec_end(
TurnItem::CommandExecution(CommandExecutionItem {
id: ctx.call_id.to_string(),
model_context: ctx.model_context.cloned(),
sandbox_type: ctx.sandbox_type,
plugin_id,
script_path,
process_id: exec_input.process_id.map(str::to_owned),
@@ -202,6 +202,7 @@ pub(crate) fn spawn_exit_watcher(
if let Some(message) = process.failure_message() {
drop(plugin_metrics_sidecar);
emit_failed_exec_end_for_unified_exec(
process.sandbox_type(),
session_ref,
turn_ref,
model_info,
@@ -229,6 +230,7 @@ pub(crate) fn spawn_exit_watcher(
)
.await;
emit_exec_end_for_unified_exec(
process.sandbox_type(),
session_ref,
turn_ref,
model_info,
@@ -342,6 +344,7 @@ impl Emitter {
/// text when the transcript is empty.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn emit_exec_end_for_unified_exec(
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
session_ref: Arc<Session>,
turn_ref: Arc<TurnContext>,
model_info: Arc<ModelInfo>,
@@ -365,13 +368,14 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
duration,
timed_out,
};
let event_ctx = ToolEventCtx::new(
let mut event_ctx = ToolEventCtx::new(
session_ref.as_ref(),
turn_ref.as_ref(),
&model_info,
&call_id,
/*turn_diff_tracker*/ None,
);
event_ctx.sandbox_type = sandbox_type;
let emitter = ToolEmitter::unified_exec(
&command,
cwd,
@@ -392,6 +396,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
#[allow(clippy::too_many_arguments)]
pub(crate) async fn emit_failed_exec_end_for_unified_exec(
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
session_ref: Arc<Session>,
turn_ref: Arc<TurnContext>,
model_info: Arc<ModelInfo>,
@@ -423,13 +428,14 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
duration,
timed_out: false,
};
let event_ctx = ToolEventCtx::new(
let mut event_ctx = ToolEventCtx::new(
session_ref.as_ref(),
turn_ref.as_ref(),
&model_info,
&call_id,
/*turn_diff_tracker*/ None,
);
event_ctx.sandbox_type = sandbox_type;
let emitter = ToolEmitter::unified_exec(
&command,
cwd,
+6 -6
View File
@@ -97,7 +97,7 @@ pub(crate) struct UnifiedExecProcess {
state_tx: watch::Sender<ProcessState>,
state_rx: watch::Receiver<ProcessState>,
output_task: Option<JoinHandle<()>>,
sandbox_type: SandboxType,
sandbox_type: Option<SandboxType>,
timed_out: AtomicBool,
_spawn_lifecycle: Option<SpawnLifecycleHandle>,
// The shell may still need to replay this file after process startup returns.
@@ -117,7 +117,7 @@ impl std::fmt::Debug for UnifiedExecProcess {
impl UnifiedExecProcess {
fn new(
process_handle: ProcessHandle,
sandbox_type: SandboxType,
sandbox_type: Option<SandboxType>,
spawn_lifecycle: Option<SpawnLifecycleHandle>,
) -> Self {
let output = OutputHandles {
@@ -281,7 +281,7 @@ impl UnifiedExecProcess {
guard.to_bytes()
}
pub(crate) fn sandbox_type(&self) -> SandboxType {
pub(crate) fn sandbox_type(&self) -> Option<SandboxType> {
self.sandbox_type
}
@@ -309,7 +309,7 @@ impl UnifiedExecProcess {
text: &str,
) -> Result<(), UnifiedExecError> {
let executor_reported_denial = self.state_rx.borrow().sandbox_denied;
let sandbox_type = self.sandbox_type();
let sandbox_type = self.sandbox_type().unwrap_or(SandboxType::None);
if !self.has_exited() || (!executor_reported_denial && sandbox_type == SandboxType::None) {
return Ok(());
}
@@ -354,7 +354,7 @@ impl UnifiedExecProcess {
let output_rx = codex_utils_pty::combine_output_receivers(stdout_rx, stderr_rx);
let mut managed = Self::new(
ProcessHandle::Local(Box::new(process_handle)),
sandbox_type,
Some(sandbox_type),
Some(spawn_lifecycle),
);
managed.output_task = Some(Self::spawn_local_output_task(
@@ -403,7 +403,7 @@ impl UnifiedExecProcess {
let process_handle = ProcessHandle::ExecServer(Arc::clone(&started.process));
// Older peers do not report this field. In that case, skip local
// classification rather than attributing a violation to a guessed backend.
let sandbox_type = started.sandbox_type.unwrap_or(SandboxType::None);
let sandbox_type = started.sandbox_type;
let mut managed = Self::new(process_handle, sandbox_type, /*spawn_lifecycle*/ None);
let output_handles = managed.output_handles().clone();
managed.output_task = Some(Self::spawn_exec_server_output_task(
@@ -394,6 +394,7 @@ fn fail_process_with_message(process: &UnifiedExecProcess, message: String) -> U
#[allow(clippy::too_many_arguments)]
async fn emit_failed_initial_exec_end_if_unstored(
process_started_alive: bool,
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
context: &UnifiedExecContext,
request: &ExecCommandRequest,
cwd: PathUri,
@@ -408,6 +409,7 @@ async fn emit_failed_initial_exec_end_if_unstored(
}
emit_failed_exec_end_for_unified_exec(
sandbox_type,
Arc::clone(&context.session),
Arc::clone(&context.step_context.turn),
Arc::clone(&context.step_context.settings.model_info),
@@ -677,6 +679,7 @@ impl UnifiedExecProcessManager {
.await;
emit_failed_initial_exec_end_if_unstored(
process_started_alive,
process.sandbox_type(),
context,
&request,
cwd.clone(),
@@ -698,6 +701,7 @@ impl UnifiedExecProcessManager {
.await;
emit_failed_initial_exec_end_if_unstored(
process_started_alive,
process.sandbox_type(),
context,
&request,
cwd.clone(),
@@ -776,6 +780,7 @@ impl UnifiedExecProcessManager {
if let Err(message) = finish_result {
emit_failed_initial_exec_end_if_unstored(
process_started_alive,
process.sandbox_type(),
context,
&request,
cwd.clone(),
@@ -795,6 +800,7 @@ impl UnifiedExecProcessManager {
.finish_plugin_metrics(context, exit)
.await;
emit_exec_end_for_unified_exec(
process.sandbox_type(),
Arc::clone(&context.session),
Arc::clone(&context.step_context.turn),
Arc::clone(&context.step_context.settings.model_info),
@@ -482,6 +482,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
emit_failed_initial_exec_end_if_unstored(
/*process_started_alive*/ false,
Some(codex_protocol::sandbox::SandboxType::WindowsMxc),
&context,
&request,
#[allow(deprecated)]
@@ -505,6 +506,10 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
panic!("expected CommandExecution item");
};
assert_eq!(item.id, "call-unified-denied");
assert_eq!(
item.sandbox_type,
Some(codex_protocol::sandbox::SandboxType::WindowsMxc)
);
assert_eq!(
item.status,
codex_protocol::items::CommandExecutionStatus::Failed
+16 -13
View File
@@ -88,7 +88,7 @@ impl ExecProcess for MockExecProcess {
pub(super) async fn remote_process(
write_status: WriteStatus,
terminate_error: Option<String>,
sandbox_type: codex_sandboxing::SandboxType,
sandbox_type: impl Into<Option<codex_sandboxing::SandboxType>>,
) -> UnifiedExecProcess {
let (wake_tx, _wake_rx) = watch::channel(0);
let started = StartedExecProcess {
@@ -101,7 +101,7 @@ pub(super) async fn remote_process(
terminate_error,
wake_tx,
}),
sandbox_type: Some(sandbox_type),
sandbox_type: sandbox_type.into(),
};
UnifiedExecProcess::from_exec_server_started(started)
@@ -198,15 +198,18 @@ async fn remote_terminate_confirmed_updates_state_on_success_only() {
#[tokio::test]
async fn remote_process_preserves_executor_sandbox_type() {
let process = remote_process(
WriteStatus::Accepted,
/*terminate_error*/ None,
codex_sandboxing::SandboxType::LinuxSeccomp,
)
.await;
assert_eq!(
process.sandbox_type(),
codex_sandboxing::SandboxType::LinuxSeccomp
);
use codex_sandboxing::SandboxType;
for sandbox_type in [
None,
Some(SandboxType::None),
Some(SandboxType::LinuxSeccomp),
] {
let process = remote_process(
WriteStatus::Accepted,
/*terminate_error*/ None,
sandbox_type,
)
.await;
assert_eq!(process.sandbox_type(), sandbox_type);
}
}
@@ -170,6 +170,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
let command_item = ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command: "ls".to_string(),
cwd: test_path_buf("/tmp/project").abs().into(),
@@ -213,6 +214,7 @@ fn command_execution_started_and_completed_translate_to_thread_events() {
ItemCompletedNotification {
item: ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command: "ls".to_string(),
cwd: test_path_buf("/tmp/project").abs().into(),
@@ -1415,6 +1417,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
processor.collect_thread_events(ServerNotification::ItemStarted(ItemStartedNotification {
item: ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command: "ls".to_string(),
cwd: test_path_buf("/tmp/project").abs().into(),
@@ -1458,6 +1461,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
items_view: codex_app_server_protocol::TurnItemsView::Full,
items: vec![ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command: "ls".to_string(),
cwd: test_path_buf("/tmp/project").abs().into(),
+5
View File
@@ -242,6 +242,11 @@ pub struct ModelInvocationContext {
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct CommandExecutionItem {
/// Observed process backend for live analytics; None means unknown or not launched.
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
pub sandbox_type: Option<crate::sandbox::SandboxType>,
#[serde(skip)]
#[schemars(skip)]
#[ts(skip)]
+2
View File
@@ -5581,6 +5581,7 @@ mod tests {
started_at_ms: 10,
item: TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: "exec-1".into(),
plugin_id: Some("sample@openai-curated".into()),
script_path: Some("scripts/run.py".into()),
@@ -5608,6 +5609,7 @@ mod tests {
completed_at_ms: 20,
item: TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: "exec-1".into(),
plugin_id: Some("sample@openai-curated".into()),
script_path: Some("scripts/run.py".into()),
@@ -268,6 +268,7 @@ pub(super) fn completed_item(
EventMsg::ExecCommandEnd(event) => Some((
TurnItem::CommandExecution(CommandExecutionItem {
model_context: None,
sandbox_type: None,
id: event.call_id.clone(),
plugin_id: event.plugin_id.clone(),
script_path: event.script_path.clone(),
@@ -11,6 +11,7 @@ fn agent_status_uses_bounded_buffered_activity() {
ItemCompletedNotification {
item: ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "command-1".to_string(),
command: "cargo test -p codex-tui".to_string(),
cwd: AbsolutePathBuf::try_from("/workspace")
@@ -212,6 +212,7 @@ mod tests {
fn command_execution(call_id: &str) -> ThreadItem {
ThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: call_id.to_string(),
command: "true".to_string(),
cwd: AbsolutePathBuf::current_dir().expect("current dir").into(),
@@ -1132,6 +1132,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
started_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command: command.clone(),
cwd: test_path_buf("/tmp").abs().into(),
@@ -1157,6 +1158,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() {
completed_at_ms: 0,
item: AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "cmd-1".to_string(),
command,
cwd: test_path_buf("/tmp").abs().into(),
@@ -240,6 +240,7 @@ async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() {
let command = vec!["bash".to_string(), "-lc".to_string(), script.to_string()];
let mut item = AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: id.to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: chat.config.cwd.clone().into(),
@@ -334,6 +335,7 @@ async fn replayed_commands_preserve_individual_output_and_failure_status() {
let replayed_command =
|id: &str, output: &str, source: ExecCommandSource| AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: id.to_string(),
command: format!("printf {output}"),
cwd: cwd.clone().into(),
@@ -783,6 +785,7 @@ async fn exec_end_without_begin_uses_event_command() {
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "call-orphan".to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.into(),
@@ -968,6 +968,7 @@ pub(super) fn begin_exec_with_source(
.collect();
let item = AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: call_id.to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: chat.config.cwd.clone().into(),
@@ -994,6 +995,7 @@ pub(super) fn begin_unified_exec_startup(
let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()];
let item = AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: call_id.to_string(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: chat.config.cwd.clone().into(),
@@ -1229,6 +1231,7 @@ pub(super) fn end_exec(
chat,
AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id,
command,
cwd,
@@ -17,6 +17,7 @@ async fn older_tool_projection_matches_initial_replay() {
plugin_id: None,
script_path: None,
model_context: None,
sandbox_type: None,
command: command.to_string(),
cwd: chat.config.cwd.clone().into(),
process_id: None,
@@ -42,6 +43,7 @@ async fn older_tool_projection_matches_initial_replay() {
plugin_id: None,
script_path: None,
model_context: None,
sandbox_type: None,
command: command.clone(),
cwd: chat.config.cwd.clone().into(),
process_id: None,
@@ -5678,6 +5678,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "c1".into(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.clone().into(),
@@ -5696,6 +5697,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
&mut chat,
AppServerThreadItem::CommandExecution {
model_context: None,
sandbox_type: None,
id: "c1".into(),
command: codex_shell_command::parse_command::shlex_join(&command),
cwd: cwd.into(),
@@ -15,6 +15,7 @@ fn command_item(status: CommandExecutionStatus) -> ThreadItem {
plugin_id: None,
script_path: None,
model_context: None,
sandbox_type: None,
command: "cargo check".to_string(),
cwd: LegacyAppPathString::from_string("/tmp/project"),
process_id: None,