mirror of
https://github.com/openai/codex.git
synced 2026-09-28 08:43:01 +08:00
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:
@@ -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)]
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user