From b947695a9811b0717b3ff919fa7ebcf132ac8a40 Mon Sep 17 00:00:00 2001 From: "Adam Perry @ OpenAI" Date: Wed, 17 Jun 2026 10:51:41 -0700 Subject: [PATCH 1/2] Tell codex to avoid changing rollout format. (#28632) Just adds a requirement to the path-types skill to nudge Codex away from touching rollout types while migrating paths. --- .codex/skills/path-types/SKILL.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.codex/skills/path-types/SKILL.md b/.codex/skills/path-types/SKILL.md index ccf1a9f41d..96caa98058 100644 --- a/.codex/skills/path-types/SKILL.md +++ b/.codex/skills/path-types/SKILL.md @@ -31,6 +31,7 @@ Keep these requirements in mind while migrating code to conform with the above g * path reasoning must work before the related environment has come online * URIs cannot explicitly encode the executor’s path convention or operating system * users must not configure the environment’s OS/path convention explicitly +* URIs should not yet be stored in rollouts It is OK if the conversion between paths and URIs is somewhat lossy as long as it will do the right thing for real users. From 1391d786bc63262aeaac2288fffbc7acc316e01d Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 17 Jun 2026 18:52:43 +0100 Subject: [PATCH 2/2] 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 7c634ce370..4aae796562 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 323247e16c..4e096cbe04 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 70f54f35ce..97934dd5bd 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 60966655bd..6251f7dc7a 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 0e9100836a..230e5b7864 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 7611c27ff1..801e9b46b8 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 502eb62e10..5638e3bd41 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 6edaa045b9..3b5e7d8a3f 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 d1a4648588..62c40555e6 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 868cef81fc..a78876a343 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 52d74b8198..9ce815bafe 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 4b9b387aaa..97372d6ae1 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 76b02a6d6e..dca58867c8 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 16398a2f28..b3f0d6c43c 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 f97e81c6e8..21a86f224e 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 01d6246ce7..cf19123591 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 9cc808a5b6..b05a745526 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 6cfa3b2dfc..f88c2be3d4 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 f2df497089..70390cb3c2 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 6d42e7c84d..2b00147612 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 0000000000..eaa7adf48c --- /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 eb23fc0ced..0b141faabc 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 c4ec218dc9..d605b640ca 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 ace096359c..314f6bd923 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 e4ab347776..d61e84dcec 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 4f90dbe9cf..406cd687ff 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 87538251aa..6a88c4ccf0 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 48dc1d0848..6b76a65186 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 c83920556c..f436ac51b3 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 57e0959d56..ba183225b8 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 b321aeb159..3cc135da74 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 e492024765..951c310d1f 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 6495b8f1fe..60973c13d2 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 301f918418..f8ecb3d0aa 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 bde795ca95..5c9b92b517 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 ca1c9bc681..60eb2d891d 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 7e766d67ed..fbb12551fd 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 f748d94e81..1667501335 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 317942485c..7445162759 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 3001563451..99b0c65207 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,