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.
This commit is contained in:
jif
2026-06-17 18:52:43 +01:00
committed by GitHub
Unverified
parent b947695a98
commit 1391d786bc
40 changed files with 215 additions and 14 deletions
@@ -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()),
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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, /**
@@ -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()),
@@ -1321,6 +1321,9 @@ pub struct CommandExecutionRequestApprovalParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub approval_id: Option<String>,
/// Environment in which the command will run.
#[serde(default)]
pub environment_id: Option<String>,
/// Optional explanatory reason (e.g. request for network access).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
@@ -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}");
}
+1 -1
View File
@@ -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.
@@ -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,
@@ -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()),
@@ -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()),
@@ -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
+2
View File
@@ -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,
@@ -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(),
+2
View File
@@ -2039,6 +2039,7 @@ impl Session {
turn_context: &TurnContext,
call_id: String,
approval_id: Option<String>,
environment_id: Option<String>,
command: Vec<String>,
cwd: AbsolutePathBuf,
reason: Option<String>,
@@ -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,
@@ -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(),
@@ -93,6 +93,7 @@ pub struct ShellRuntime {
#[derive(serde::Serialize, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct ApprovalKey {
environment_id: String,
command: Vec<String>,
cwd: AbsolutePathBuf,
sandbox_permissions: SandboxPermissions,
@@ -127,6 +128,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
fn approval_keys(&self, req: &ShellRequest) -> Vec<Self::ApprovalKey> {
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<ShellRequest> 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<ShellRequest> for ShellRuntime {
turn,
call_id,
/*approval_id*/ None,
environment_id,
command,
cwd,
reason,
@@ -325,3 +329,7 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
Ok(out)
}
}
#[cfg(test)]
#[path = "shell_tests.rs"]
mod tests;
@@ -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<crate::session::session::Session>,
turn: Arc<crate::session::turn_context::TurnContext>,
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,
@@ -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,
@@ -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);
}
@@ -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<String>,
pub cwd: AbsolutePathBuf,
pub tty: bool,
@@ -135,6 +136,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
fn approval_keys(&self, req: &UnifiedExecRequest) -> Vec<Self::ApprovalKey> {
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<UnifiedExecRequest> 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<UnifiedExecRequest> 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");
@@ -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,
+10
View File
@@ -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<String>,
#[ts(type = "number")]
pub started_at_ms: i64,
/// The command to be executed.
@@ -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()),
@@ -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()),
+1
View File
@@ -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()),
+1
View File
@@ -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()),
+1
View File
@@ -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()
+2
View File
@@ -26,6 +26,8 @@ pub(crate) struct ExecApprovalRequestEvent {
pub(crate) approval_id: Option<String>,
#[serde(default)]
pub(crate) turn_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) environment_id: Option<String>,
pub(crate) command: Vec<String>,
pub(crate) cwd: AbsolutePathBuf,
pub(crate) reason: Option<String>,
@@ -74,6 +74,7 @@ pub(crate) enum ApprovalRequest {
thread_id: ThreadId,
thread_label: Option<String>,
id: String,
environment_id: Option<String>,
command: Vec<String>,
reason: Option<String>,
available_decisions: Vec<CommandExecutionApprovalDecision>,
@@ -675,6 +676,7 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
match request {
ApprovalRequest::Exec {
thread_label,
environment_id,
reason,
command,
network_approval_context,
@@ -689,6 +691,13 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
]));
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(),
+1
View File
@@ -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![
+1
View File
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
]
}
@@ -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(
@@ -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()),
@@ -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,