mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
@@ -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()),
|
||||
|
||||
+8
@@ -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"
|
||||
},
|
||||
|
||||
+8
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+3
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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,
|
||||
|
||||
+17
-13
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user