From 1391d786bc63262aeaac2288fffbc7acc316e01d Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 17 Jun 2026 18:52:43 +0100 Subject: [PATCH] Scope command approvals by execution environment (#28738) ## Why Command approval cache keys included the command and working directory, but not the execution environment. An approval for `/workspace` locally could therefore be reused for the same command and path on an executor. ## What changed - Include the selected environment ID in shell and unified-exec approval cache keys. - Carry that ID through the normal command approval request so clients can show which environment is being approved. - Expose the environment through app-server as a required nullable `environmentId` and show it in the inline TUI approval prompt. - Keep older recorded approval events compatible when the environment is absent. For example, `echo ok` in local `/workspace` and `echo ok` in executor `/workspace` now produce different approval keys and separate prompts. ## Scope This PR does not change network approvals, Guardian review actions, MCP elicitation, full-screen TUI rendering, or environment-ID validation. Remote `shell_command` execution itself remains in #28722; this PR only makes its approval key environment-aware. --- .../analytics/src/analytics_client_tests.rs | 1 + ...CommandExecutionRequestApprovalParams.json | 8 ++++ .../schema/json/ServerRequest.json | 8 ++++ .../codex_app_server_protocol.schemas.json | 8 ++++ .../CommandExecutionRequestApprovalParams.ts | 3 ++ .../src/protocol/common.rs | 1 + .../src/protocol/v2/item.rs | 3 ++ codex-rs/app-server-test-client/src/lib.rs | 4 ++ codex-rs/app-server/README.md | 2 +- .../app-server/src/bespoke_event_handling.rs | 2 + codex-rs/app-server/src/outgoing_message.rs | 1 + codex-rs/app-server/src/transport_tests.rs | 2 + .../app-server/tests/suite/v2/turn_start.rs | 1 + codex-rs/core/src/codex_delegate.rs | 2 + codex-rs/core/src/codex_delegate_tests.rs | 1 + codex-rs/core/src/session/mod.rs | 2 + codex-rs/core/src/tools/network_approval.rs | 1 + codex-rs/core/src/tools/runtimes/shell.rs | 8 ++++ .../tools/runtimes/shell/unix_escalation.rs | 5 +++ .../runtimes/shell/unix_escalation_tests.rs | 4 ++ .../core/src/tools/runtimes/shell_tests.rs | 42 +++++++++++++++++++ .../core/src/tools/runtimes/unified_exec.rs | 23 ++++++++++ codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/protocol/src/approvals.rs | 10 +++++ codex-rs/tui/src/app/app_server_requests.rs | 2 + .../tui/src/app/pending_interactive_replay.rs | 1 + codex-rs/tui/src/app/tests.rs | 1 + codex-rs/tui/src/app/thread_events.rs | 1 + codex-rs/tui/src/app/thread_routing.rs | 1 + codex-rs/tui/src/approval_events.rs | 2 + .../tui/src/bottom_pane/approval_overlay.rs | 22 ++++++++++ codex-rs/tui/src/bottom_pane/mod.rs | 1 + codex-rs/tui/src/chatwidget.rs | 1 + codex-rs/tui/src/chatwidget/interrupts.rs | 1 + .../src/chatwidget/tests/approval_requests.rs | 9 ++++ .../tui/src/chatwidget/tests/exec_flow.rs | 8 ++++ ...al_requests__exec_approval_modal_exec.snap | 30 +++++++------ .../src/chatwidget/tests/status_and_layout.rs | 1 + .../src/chatwidget/tests/terminal_title.rs | 4 ++ codex-rs/tui/src/chatwidget/tool_requests.rs | 1 + 40 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 codex-rs/core/src/tools/runtimes/shell_tests.rs diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 7c634ce37..4aae79656 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -861,6 +861,7 @@ fn sample_command_approval_request(request_id: i64, approval_id: Option<&str>) - item_id: "item-1".to_string(), started_at_ms: 1_000, approval_id: approval_id.map(str::to_string), + environment_id: None, reason: None, network_approval_context: None, command: Some("echo hi".to_string()), diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 323247e16..4e096cbe0 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -555,6 +555,14 @@ ], "description": "The command's working directory." }, + "environmentId": { + "default": null, + "description": "Environment in which the command will run.", + "type": [ + "string", + "null" + ] + }, "itemId": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 70f54f35c..97934dd5b 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -379,6 +379,14 @@ ], "description": "The command's working directory." }, + "environmentId": { + "default": null, + "description": "Environment in which the command will run.", + "type": [ + "string", + "null" + ] + }, "itemId": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 60966655b..6251f7dc7 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -2386,6 +2386,14 @@ ], "description": "The command's working directory." }, + "environmentId": { + "default": null, + "description": "Environment in which the command will run.", + "type": [ + "string", + "null" + ] + }, "itemId": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index 0e9100836..230e5b786 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -20,6 +20,9 @@ startedAtMs: number, /** * (a UUID) used to disambiguate routing. */ approvalId?: string | null, /** + * Environment in which the command will run. + */ +environmentId: string | null, /** * Optional explanatory reason (e.g. request for network access). */ reason?: string | null, /** diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 7611c27ff..801e9b46b 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -3538,6 +3538,7 @@ mod tests { item_id: "call_123".to_string(), started_at_ms: 0, approval_id: None, + environment_id: None, reason: None, network_approval_context: None, command: Some("cat file".to_string()), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 502eb62e1..5638e3bd4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -1321,6 +1321,9 @@ pub struct CommandExecutionRequestApprovalParams { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub approval_id: Option, + /// Environment in which the command will run. + #[serde(default)] + pub environment_id: Option, /// Optional explanatory reason (e.g. request for network access). #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index 6edaa045b..3b5e7d8a3 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -2058,6 +2058,7 @@ impl CodexClient { item_id, started_at_ms: _, approval_id, + environment_id, reason, network_approval_context, command, @@ -2075,6 +2076,9 @@ impl CodexClient { ); self.command_approval_count += 1; self.command_approval_item_ids.push(item_id.clone()); + if let Some(environment_id) = environment_id.as_deref() { + println!("< environment: {environment_id}"); + } if let Some(reason) = reason.as_deref() { println!("< reason: {reason}"); } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index d1a464858..62c40555e 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -1429,7 +1429,7 @@ Certain actions (shell commands or modifying files) may require explicit user ap Order of messages: 1. `item/started` — shows the pending `commandExecution` item with `command`, `cwd`, and other fields so you can render the proposed action. -2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, optionally `approvalId` (for subcommand callbacks), and `reason`. For normal command approvals, it also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. +2. `item/commandExecution/requestApproval` (request) — carries the same `itemId`, `threadId`, `turnId`, the nullable `environmentId` where the command will run, optionally `approvalId` (for subcommand callbacks), and `reason`. New shell and unified-exec approvals set `environmentId`; older events that do not provide one are exposed as `null`. For normal command approvals, the request also includes `command`, `cwd`, and `commandActions` for friendly display. When `initialize.params.capabilities.experimentalApi = true`, it may also include experimental `additionalPermissions` describing requested per-command sandbox access; any filesystem paths in that payload are absolute on the wire, and network access is represented as `additionalPermissions.network.enabled`. For network-only approvals, those command fields may be omitted and `networkApprovalContext` is provided instead. Optional persistence hints may also be included via `proposedExecpolicyAmendment` and `proposedNetworkPolicyAmendments`. Clients can prefer `availableDecisions` when present to render the exact set of choices the server wants to expose, while still falling back to the older heuristics if it is omitted. 3. Client response — for example `{ "decision": "accept" }`, `{ "decision": "acceptForSession" }`, `{ "decision": { "acceptWithExecpolicyAmendment": { "execpolicy_amendment": [...] } } }`, `{ "decision": { "applyNetworkPolicyAmendment": { "network_policy_amendment": { "host": "example.com", "action": "allow" } } } }`, `{ "decision": "decline" }`, or `{ "decision": "cancel" }`. 4. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt. 5. `item/completed` — final `commandExecution` item with `status: "completed" | "failed" | "declined"` and execution output. Render this as the authoritative result. diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 868cef81f..a78876a34 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -550,6 +550,7 @@ pub(crate) async fn apply_bespoke_event_handling( call_id, approval_id, turn_id, + environment_id, started_at_ms, command, cwd, @@ -626,6 +627,7 @@ pub(crate) async fn apply_bespoke_event_handling( item_id: call_id.clone(), started_at_ms, approval_id: approval_id.clone(), + environment_id, reason, network_approval_context, command, diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 52d74b819..9ce815baf 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -977,6 +977,7 @@ mod tests { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: None, + environment_id: None, reason: None, network_approval_context: None, command: Some("echo hi".to_string()), diff --git a/codex-rs/app-server/src/transport_tests.rs b/codex-rs/app-server/src/transport_tests.rs index 4b9b387aa..97372d6ae 100644 --- a/codex-rs/app-server/src/transport_tests.rs +++ b/codex-rs/app-server/src/transport_tests.rs @@ -250,6 +250,7 @@ async fn command_execution_request_approval_strips_additional_permissions_withou item_id: "call_123".to_string(), started_at_ms: 0, approval_id: None, + environment_id: None, reason: Some("Need extra read access".to_string()), network_approval_context: None, command: Some("cat file".to_string()), @@ -315,6 +316,7 @@ async fn command_execution_request_approval_keeps_additional_permissions_with_ca item_id: "call_123".to_string(), started_at_ms: 0, approval_id: None, + environment_id: None, reason: Some("Need extra read access".to_string()), network_approval_context: None, command: Some("cat file".to_string()), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 76b02a6d6..dca58867c 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -2067,6 +2067,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { panic!("expected CommandExecutionRequestApproval request"); }; assert_eq!(params.item_id, "call1"); + assert_eq!(params.environment_id.as_deref(), Some("local")); let resolved_request_id = request_id.clone(); // Approve and wait for task completion diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index 16398a2f2..b3f0d6c43 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -460,6 +460,7 @@ async fn handle_exec_approval( let ExecApprovalRequestEvent { call_id, approval_id, + environment_id, command, cwd, reason, @@ -505,6 +506,7 @@ async fn handle_exec_approval( parent_ctx, call_id, approval_id, + environment_id, command, cwd, reason, diff --git a/codex-rs/core/src/codex_delegate_tests.rs b/codex-rs/core/src/codex_delegate_tests.rs index f97e81c6e..21a86f224 100644 --- a/codex-rs/core/src/codex_delegate_tests.rs +++ b/codex-rs/core/src/codex_delegate_tests.rs @@ -322,6 +322,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f call_id: "command-item-1".to_string(), approval_id: Some("callback-approval-1".to_string()), turn_id: "child-turn-1".to_string(), + environment_id: Some("remote".to_string()), started_at_ms: 0, command: vec!["rm".to_string(), "-rf".to_string(), "tmp".to_string()], cwd: test_path_buf("/tmp").abs(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 01d6246ce..cf1912359 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2039,6 +2039,7 @@ impl Session { turn_context: &TurnContext, call_id: String, approval_id: Option, + environment_id: Option, command: Vec, cwd: AbsolutePathBuf, reason: Option, @@ -2091,6 +2092,7 @@ impl Session { call_id, approval_id, turn_id: turn_context.sub_id.clone(), + environment_id, started_at_ms: now_unix_timestamp_ms(), command, cwd, diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index 9cc808a5b..b05a74552 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -524,6 +524,7 @@ impl NetworkApprovalService { turn_context.as_ref(), guardian_approval_id, /*approval_id*/ None, + /*environment_id*/ None, prompt_command, #[allow(deprecated)] turn_context.cwd.clone(), diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 6cfa3b2df..f88c2be3d 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -93,6 +93,7 @@ pub struct ShellRuntime { #[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct ApprovalKey { + environment_id: String, command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: SandboxPermissions, @@ -127,6 +128,7 @@ impl Approvable for ShellRuntime { fn approval_keys(&self, req: &ShellRequest) -> Vec { vec![ApprovalKey { + environment_id: req.turn_environment.environment_id.clone(), command: canonicalize_command_for_approval(&req.command), cwd: req.cwd.clone(), sandbox_permissions: req.sandbox_permissions, @@ -142,6 +144,7 @@ impl Approvable for ShellRuntime { let keys = self.approval_keys(req); let command = req.command.clone(); let cwd = req.cwd.clone(); + let environment_id = Some(req.turn_environment.environment_id.clone()); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); let session = ctx.session; @@ -173,6 +176,7 @@ impl Approvable for ShellRuntime { turn, call_id, /*approval_id*/ None, + environment_id, command, cwd, reason, @@ -325,3 +329,7 @@ impl ToolRuntime for ShellRuntime { Ok(out) } } + +#[cfg(test)] +#[path = "shell_tests.rs"] +mod tests; diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index f2df49708..70390cb3c 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -222,6 +222,7 @@ pub(super) async fn try_run_zsh_fork( session: Arc::clone(&ctx.session), turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), + environment_id: req.turn_environment.environment_id.clone(), tool_name: GuardianCommandSource::Shell, approval_policy: ctx.turn.approval_policy.value(), permission_profile: command_executor.permission_profile.clone(), @@ -294,6 +295,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork( session: Arc::clone(&ctx.session), turn: Arc::clone(&ctx.turn), call_id: ctx.call_id.clone(), + environment_id: req.turn_environment.environment_id.clone(), tool_name: GuardianCommandSource::UnifiedExec, approval_policy: ctx.turn.approval_policy.value(), permission_profile: exec_request.permission_profile.clone(), @@ -328,6 +330,7 @@ struct CoreShellActionProvider { session: Arc, turn: Arc, call_id: String, + environment_id: String, tool_name: GuardianCommandSource, approval_policy: AskForApproval, permission_profile: PermissionProfile, @@ -424,6 +427,7 @@ impl CoreShellActionProvider { let turn = self.turn.clone(); let call_id = self.call_id.clone(); let approval_id = Some(Uuid::new_v4().to_string()); + let environment_id = Some(self.environment_id.clone()); let source = self.tool_name; let guardian_review_id = routes_approval_to_guardian(&turn).then(new_guardian_review_id); Ok(stopwatch @@ -489,6 +493,7 @@ impl CoreShellActionProvider { &turn, call_id, approval_id, + environment_id, command, workdir.clone(), /*reason*/ None, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 6d42e7c84..2b0014761 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -425,6 +425,7 @@ async fn preapproved_additional_permissions_escalate_intercepted_exec() -> anyho session: Arc::new(session), turn: Arc::new(turn_context), call_id: "preapproved-additional-permissions".to_string(), + environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, approval_policy: AskForApproval::OnRequest, permission_profile: permission_profile.clone(), @@ -560,6 +561,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul session: std::sync::Arc::new(session), turn: std::sync::Arc::new(turn_context), call_id: "execve-hook-call".to_string(), + environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, approval_policy: AskForApproval::OnRequest, permission_profile: PermissionProfile::read_only(), @@ -770,6 +772,7 @@ prefix_rule(pattern = ["{cat_path_literal}"], decision = "allow") session: Arc::new(session), turn: Arc::new(turn_context), call_id: "deny-read-prefix-allow".to_string(), + environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, approval_policy: AskForApproval::OnRequest, permission_profile, @@ -806,6 +809,7 @@ async fn denied_reads_keep_granular_sandbox_rejection_for_escalation() -> anyhow session: Arc::new(session), turn: Arc::new(turn_context), call_id: "deny-read-granular-sandbox-reject".to_string(), + environment_id: "local".to_string(), tool_name: GuardianCommandSource::Shell, approval_policy: AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: false, diff --git a/codex-rs/core/src/tools/runtimes/shell_tests.rs b/codex-rs/core/src/tools/runtimes/shell_tests.rs new file mode 100644 index 000000000..eaa7adf48 --- /dev/null +++ b/codex-rs/core/src/tools/runtimes/shell_tests.rs @@ -0,0 +1,42 @@ +use super::*; +use codex_exec_server::Environment; +use codex_utils_path_uri::PathUri; +use std::sync::Arc; + +#[tokio::test] +async fn approval_key_includes_environment_id() { + let cwd = AbsolutePathBuf::try_from(std::env::current_dir().expect("read current dir")) + .expect("current dir is absolute"); + let mut request = ShellRequest { + command: vec!["echo".to_string(), "hello".to_string()], + turn_environment: TurnEnvironment::new( + "remote".to_string(), + Arc::new(Environment::default_for_tests()), + PathUri::from_abs_path(&cwd), + /*shell*/ None, + ), + shell_type: None, + hook_command: "echo hello".to_string(), + cwd: cwd.clone(), + timeout_ms: None, + cancellation_token: CancellationToken::new(), + env: HashMap::new(), + explicit_env_overrides: HashMap::new(), + network: None, + sandbox_permissions: SandboxPermissions::UseDefault, + additional_permissions: None, + #[cfg(unix)] + additional_permissions_preapproved: false, + justification: None, + exec_approval_requirement: ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: None, + }, + }; + let runtime = ShellRuntime::for_shell_command(ShellRuntimeBackend::ShellCommandClassic); + let original_key = runtime.approval_keys(&request); + request.turn_environment.environment_id = "other".to_string(); + let other_key = runtime.approval_keys(&request); + + assert_ne!(original_key, other_key); +} diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index eb23fc0ce..0b141faab 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -83,6 +83,7 @@ pub struct UnifiedExecRequest { /// unified-exec launches. #[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)] pub struct UnifiedExecApprovalKey { + pub environment_id: String, pub command: Vec, pub cwd: AbsolutePathBuf, pub tty: bool, @@ -135,6 +136,7 @@ impl Approvable for UnifiedExecRuntime<'_> { fn approval_keys(&self, req: &UnifiedExecRequest) -> Vec { vec![UnifiedExecApprovalKey { + environment_id: req.turn_environment.environment_id.clone(), command: canonicalize_command_for_approval(&req.command), cwd: req.cwd.clone(), tty: req.tty, @@ -154,6 +156,7 @@ impl Approvable for UnifiedExecRuntime<'_> { let call_id = ctx.call_id.to_string(); let command = req.command.clone(); let cwd = req.cwd.clone(); + let environment_id = Some(req.turn_environment.environment_id.clone()); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); let guardian_review_id = ctx.guardian_review_id.clone(); @@ -183,6 +186,7 @@ impl Approvable for UnifiedExecRuntime<'_> { turn, call_id, /*approval_id*/ None, + environment_id, command, cwd.clone(), reason, @@ -463,6 +467,25 @@ mod tests { } } + #[tokio::test] + async fn approval_key_includes_environment_id() { + let manager = UnifiedExecProcessManager::default(); + let runtime = UnifiedExecRuntime::new(&manager, UnifiedExecShellMode::Direct); + let mut request = test_request( + SandboxPermissions::UseDefault, + ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: None, + }, + ); + request.turn_environment.environment_id = "remote".to_string(); + let original_key = runtime.approval_keys(&request); + request.turn_environment.environment_id = "other".to_string(); + let other_key = runtime.approval_keys(&request); + + assert_ne!(original_key, other_key); + } + #[tokio::test] async fn unified_exec_uses_the_trusted_sandbox_cwd() { let cwd_dir = tempdir().expect("create process temp dir"); diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index c4ec218dc..d605b640c 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -220,6 +220,7 @@ async fn run_codex_tool_session_inner( let approval_id = ev.effective_approval_id(); let ExecApprovalRequestEvent { turn_id: _, + environment_id: _, started_at_ms: _, command, cwd, diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index ace096359..314f6bd92 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -229,6 +229,16 @@ pub struct ExecApprovalRequestEvent { /// Uses `#[serde(default)]` for backwards compatibility. #[serde(default)] pub turn_id: String, + /// Environment in which the command will run. + #[serde( + default, + rename = "environmentId", + alias = "environment_id", + skip_serializing_if = "Option::is_none" + )] + #[ts(optional)] + #[ts(rename = "environmentId")] + pub environment_id: Option, #[ts(type = "number")] pub started_at_ms: i64, /// The command to be executed. diff --git a/codex-rs/tui/src/app/app_server_requests.rs b/codex-rs/tui/src/app/app_server_requests.rs index e4ab34777..d61e84dce 100644 --- a/codex-rs/tui/src/app/app_server_requests.rs +++ b/codex-rs/tui/src/app/app_server_requests.rs @@ -452,6 +452,7 @@ mod tests { item_id: "call-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: None, command: Some("ls".to_string()), @@ -791,6 +792,7 @@ mod tests { item_id: "call-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: None, command: Some("ls".to_string()), diff --git a/codex-rs/tui/src/app/pending_interactive_replay.rs b/codex-rs/tui/src/app/pending_interactive_replay.rs index 4f90dbe9c..406cd687f 100644 --- a/codex-rs/tui/src/app/pending_interactive_replay.rs +++ b/codex-rs/tui/src/app/pending_interactive_replay.rs @@ -615,6 +615,7 @@ mod tests { item_id: call_id.to_string(), started_at_ms: 0, approval_id: approval_id.map(str::to_string), + environment_id: None, reason: None, network_approval_context: None, command: Some("echo hi".to_string()), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 87538251a..6a88c4ccf 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -4690,6 +4690,7 @@ fn exec_approval_request( item_id: item_id.to_string(), started_at_ms: 0, approval_id: approval_id.map(str::to_string), + environment_id: None, reason: Some("needs approval".to_string()), network_approval_context: None, command: Some("echo hello".to_string()), diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 48dc1d084..6b76a6518 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -488,6 +488,7 @@ mod tests { item_id: item_id.to_string(), started_at_ms: 0, approval_id: approval_id.map(str::to_string), + environment_id: None, reason: Some("needs approval".to_string()), network_approval_context: None, command: Some("echo hello".to_string()), diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index c83920556..f436ac51b 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -226,6 +226,7 @@ impl App { .approval_id .clone() .unwrap_or_else(|| params.item_id.clone()), + environment_id: params.environment_id.clone(), command: params .command .as_deref() diff --git a/codex-rs/tui/src/approval_events.rs b/codex-rs/tui/src/approval_events.rs index 57e0959d5..ba183225b 100644 --- a/codex-rs/tui/src/approval_events.rs +++ b/codex-rs/tui/src/approval_events.rs @@ -26,6 +26,8 @@ pub(crate) struct ExecApprovalRequestEvent { pub(crate) approval_id: Option, #[serde(default)] pub(crate) turn_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(crate) environment_id: Option, pub(crate) command: Vec, pub(crate) cwd: AbsolutePathBuf, pub(crate) reason: Option, diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index b321aeb15..3cc135da7 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -74,6 +74,7 @@ pub(crate) enum ApprovalRequest { thread_id: ThreadId, thread_label: Option, id: String, + environment_id: Option, command: Vec, reason: Option, available_decisions: Vec, @@ -675,6 +676,7 @@ fn build_header(request: &ApprovalRequest) -> Box { match request { ApprovalRequest::Exec { thread_label, + environment_id, reason, command, network_approval_context, @@ -689,6 +691,13 @@ fn build_header(request: &ApprovalRequest) -> Box { ])); header.push(Line::from("")); } + if let Some(environment_id) = environment_id { + header.push(Line::from(vec![ + "Environment: ".into(), + environment_id.clone().bold(), + ])); + header.push(Line::from("")); + } if let Some(reason) = reason { header.push(Line::from(vec!["Reason: ".into(), reason.clone().italic()])); header.push(Line::from("")); @@ -1220,6 +1229,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string(), "hi".to_string()], reason: Some("reason".to_string()), available_decisions: vec![ @@ -1360,6 +1370,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string(), "hi".to_string()], reason: None, available_decisions: vec![ @@ -1403,6 +1414,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".to_string(), + environment_id: None, command: vec!["curl".to_string(), "https://example.com".to_string()], reason: None, available_decisions: vec![ @@ -1477,6 +1489,7 @@ mod tests { thread_id, thread_label: Some("Robie [explorer]".to_string()), id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string(), "hi".to_string()], reason: None, available_decisions: vec![ @@ -1511,6 +1524,7 @@ mod tests { thread_id, thread_label: Some("Robie [explorer]".to_string()), id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string(), "hi".to_string()], reason: None, available_decisions: vec![ @@ -1549,6 +1563,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: Some("Robie [explorer]".to_string()), id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string(), "hi".to_string()], reason: None, available_decisions: vec![ @@ -1577,6 +1592,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".to_string(), + environment_id: None, command: vec!["echo".to_string()], reason: None, available_decisions: vec![ @@ -1629,6 +1645,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".to_string(), + environment_id: None, command: vec!["curl".to_string(), "https://example.com".to_string()], reason: None, available_decisions: vec![ @@ -1668,6 +1685,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".into(), + environment_id: None, command, reason: None, available_decisions: vec![ @@ -1966,6 +1984,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".into(), + environment_id: None, command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: None, available_decisions: vec![ @@ -2022,6 +2041,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".into(), + environment_id: None, command: vec!["cat".into(), "/tmp/readme.txt".into()], reason: Some("need filesystem access".into()), available_decisions: vec![ @@ -2102,6 +2122,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".into(), + environment_id: None, command: vec!["curl".into(), "https://example.com".into()], reason: Some("network request blocked".into()), available_decisions: vec![ @@ -2238,6 +2259,7 @@ mod tests { thread_id: ThreadId::new(), thread_label: None, id: "test".into(), + environment_id: None, command: vec![ "network-access".to_string(), "https://example.com:8443".to_string(), diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index e49202476..951c310d1 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -1896,6 +1896,7 @@ mod tests { thread_id: codex_protocol::ThreadId::new(), thread_label: None, id: "1".to_string(), + environment_id: None, command: vec!["echo".into(), "ok".into()], reason: None, available_decisions: vec![ diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6495b8f1f..60973c13d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -852,6 +852,7 @@ fn exec_approval_request_from_params( additional_permissions: params.additional_permissions, turn_id: params.turn_id, approval_id: params.approval_id, + environment_id: params.environment_id, proposed_execpolicy_amendment: params.proposed_execpolicy_amendment, proposed_network_policy_amendments: params.proposed_network_policy_amendments, available_decisions: params.available_decisions, diff --git a/codex-rs/tui/src/chatwidget/interrupts.rs b/codex-rs/tui/src/chatwidget/interrupts.rs index 301f91841..f8ecb3d0a 100644 --- a/codex-rs/tui/src/chatwidget/interrupts.rs +++ b/codex-rs/tui/src/chatwidget/interrupts.rs @@ -161,6 +161,7 @@ mod tests { call_id: call_id.to_string(), approval_id: approval_id.map(str::to_string), turn_id: "turn".to_string(), + environment_id: None, command: vec!["true".to_string()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: None, diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index bde795ca9..5c9b92b51 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -13,6 +13,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { call_id: "call-short".into(), approval_id: Some("call-short".into()), turn_id: "turn-short".into(), + environment_id: Some("remote".to_string()), command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -57,6 +58,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: None, command: Some( @@ -98,6 +100,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext { host: "example.com".to_string(), @@ -157,6 +160,7 @@ async fn network_exec_approval_history_describes_session_host_allowance() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext { host: "example.com".to_string(), @@ -198,6 +202,7 @@ async fn network_exec_approval_history_describes_one_time_host_allowance() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext { host: "example.com".to_string(), @@ -239,6 +244,7 @@ async fn network_exec_approval_history_describes_canceled_host_request() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext { host: "example.com".to_string(), @@ -330,6 +336,7 @@ async fn exec_approval_uses_approval_id_when_present() { call_id: "call-parent".into(), approval_id: Some("approval-subcommand".into()), turn_id: "turn-short".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -372,6 +379,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { call_id: "call-multi".into(), approval_id: Some("call-multi".into()), turn_id: "turn-multi".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -423,6 +431,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { call_id: "call-long".into(), approval_id: Some("call-long".into()), turn_id: "turn-long".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), long], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: None, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index ca1c9bc68..60eb2d891 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -10,6 +10,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() { call_id: "call-short".into(), approval_id: Some("call-short".into()), turn_id: "turn-short".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -56,6 +57,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() { item_id: "item-1".to_string(), started_at_ms: 0, approval_id: Some("approval-1".to_string()), + environment_id: None, reason: None, network_approval_context: None, command: Some( @@ -93,6 +95,7 @@ async fn exec_approval_uses_approval_id_when_present() { call_id: "call-parent".into(), approval_id: Some("approval-subcommand".into()), turn_id: "turn-short".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -136,6 +139,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { call_id: "call-multi".into(), approval_id: Some("call-multi".into()), turn_id: "turn-multi".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -189,6 +193,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() { call_id: "call-long".into(), approval_id: Some("call-long".into()), turn_id: "turn-long".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), long], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: None, @@ -1098,6 +1103,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> { call_id: "call-approve-cmd".into(), approval_id: Some("call-approve-cmd".into()), turn_id: "turn-approve-cmd".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some( @@ -1155,6 +1161,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> { call_id: "call-approve-cmd-noreason".into(), approval_id: Some("call-approve-cmd-noreason".into()), turn_id: "turn-approve-cmd-noreason".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello world".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: None, @@ -1201,6 +1208,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot() call_id: "call-approve-cmd-multiline-trunc".into(), approval_id: Some("call-approve-cmd-multiline-trunc".into()), turn_id: "turn-approve-cmd-multiline-trunc".into(), + environment_id: None, command: command.clone(), cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: None, diff --git a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap index 7e766d67e..fbb12551f 100644 --- a/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap +++ b/codex-rs/tui/src/chatwidget/tests/snapshots/codex_tui__chatwidget__tests__approval_requests__exec_approval_modal_exec.snap @@ -3,12 +3,14 @@ source: tui/src/chatwidget/tests/approval_requests.rs expression: "format!(\"{buf:?}\")" --- Buffer { - area: Rect { x: 0, y: 0, width: 80, height: 13 }, + area: Rect { x: 0, y: 0, width: 80, height: 15 }, content: [ " ", " ", " Would you like to run the following command? ", " ", + " Environment: remote ", + " ", " Reason: this is a test reason such as one that would be produced by the ", " model ", " ", @@ -23,17 +25,19 @@ Buffer { x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 46, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 10, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, - x: 73, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, - x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 4, y: 7, fg: Rgb(137, 180, 250), bg: Reset, underline: Reset, modifier: NONE, - x: 8, y: 7, fg: Rgb(205, 214, 244), bg: Reset, underline: Reset, modifier: NONE, - x: 20, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 0, y: 9, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, - x: 21, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 48, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, - x: 51, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, - x: 2, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 15, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, + x: 21, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 10, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 73, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC, + x: 7, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 4, y: 9, fg: Rgb(137, 180, 250), bg: Reset, underline: Reset, modifier: NONE, + x: 8, y: 9, fg: Rgb(205, 214, 244), bg: Reset, underline: Reset, modifier: NONE, + x: 20, y: 9, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 0, y: 11, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, + x: 21, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 48, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, + x: 51, y: 12, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 2, y: 14, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, ] } diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index f748d94e8..166750133 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -1864,6 +1864,7 @@ async fn status_widget_and_approval_modal_snapshot() { call_id: "call-approve-exec".into(), approval_id: Some("call-approve-exec".into()), turn_id: "turn-approve-exec".into(), + environment_id: None, command: vec!["echo".into(), "hello world".into()], cwd: test_path_buf("/tmp").abs(), reason: Some( diff --git a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs index 317942485..744516275 100644 --- a/codex-rs/tui/src/chatwidget/tests/terminal_title.rs +++ b/codex-rs/tui/src/chatwidget/tests/terminal_title.rs @@ -13,6 +13,7 @@ async fn terminal_title_shows_action_required_while_exec_approval_is_pending() { call_id: "call-action-required".into(), approval_id: Some("call-action-required".into()), turn_id: "turn-action-required".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some("need confirmation".into()), @@ -55,6 +56,7 @@ async fn terminal_title_action_required_respects_spinner_setting() { call_id: "call-no-spinner".into(), approval_id: Some("call-no-spinner".into()), turn_id: "turn-no-spinner".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some("need confirmation".into()), @@ -83,6 +85,7 @@ async fn terminal_title_action_required_blinks_when_animations_are_enabled() { call_id: "call-blink".into(), approval_id: Some("call-blink".into()), turn_id: "turn-blink".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some("need confirmation".into()), @@ -118,6 +121,7 @@ async fn terminal_title_activity_indicators_do_not_animate_when_animations_are_d call_id: "call-no-animations".into(), approval_id: Some("call-no-animations".into()), turn_id: "turn-no-animations".into(), + environment_id: None, command: vec!["bash".into(), "-lc".into(), "echo hello".into()], cwd: AbsolutePathBuf::current_dir().expect("current dir"), reason: Some("need confirmation".into()), diff --git a/codex-rs/tui/src/chatwidget/tool_requests.rs b/codex-rs/tui/src/chatwidget/tool_requests.rs index 300156345..99b0c6520 100644 --- a/codex-rs/tui/src/chatwidget/tool_requests.rs +++ b/codex-rs/tui/src/chatwidget/tool_requests.rs @@ -296,6 +296,7 @@ impl ChatWidget { thread_id: self.thread_id.unwrap_or_default(), thread_label: None, id: ev.effective_approval_id(), + environment_id: ev.environment_id, command: ev.command, reason: ev.reason, available_decisions,