Show command in sandbox permission prompts (#59362)

Summary:

- Include terminal command text in sandbox authorization metadata.
- Render the command in sandbox permission prompts, including prompts to
run outside the sandbox.
- Keep write-path details visible alongside the command when both are
present.

Tests:

- cargo test -p agent sandbox
- cargo check -p agent_ui

Release Notes:

- Improved agent sandbox permission prompts to show the command being
requested.
This commit is contained in:
MartinYe1234 authored and GitHub committed 2026-06-15 19:01:04 +00:00
1 parent 37f4c1f69d
commit 54c46bb34f
4 files changed
+121 -53

No files matched your search

+2
View File
@@ -162,6 +162,8 @@ impl SandboxPermission {
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct SandboxAuthorizationDetails {
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub network: bool,
#[serde(default)]
+4 -1
View File
@@ -5242,6 +5242,7 @@ impl ToolCallEventStream {
pub(crate) fn authorize_sandbox(
&self,
title: impl Into<String>,
command: Option<String>,
request: SandboxRequest,
cx: &mut App,
) -> Task<Result<()>> {
@@ -5251,6 +5252,7 @@ impl ToolCallEventStream {
let title = title.into();
let sandbox_authorization_details = acp_thread::SandboxAuthorizationDetails {
command,
network: request.network,
allow_fs_write_all: request.allow_fs_write_all,
unsandboxed: request.unsandboxed,
@@ -6822,12 +6824,13 @@ mod tests {
};
let authorize = cx.update(|cx| {
event_stream.authorize_sandbox("Allow write access?", request.clone(), cx)
event_stream.authorize_sandbox("Allow write access?", None, request.clone(), cx)
});
let authorization = receiver.expect_authorization().await;
let details =
acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
.expect("sandbox authorization should include request details");
assert_eq!(details.command, None);
assert_eq!(details.network, request.network);
assert_eq!(details.allow_fs_write_all, request.allow_fs_write_all);
assert_eq!(details.unsandboxed, request.unsandboxed);
+5 -1
View File
@@ -348,7 +348,9 @@ async fn run_terminal_tool(
if request.needs_escalation() {
let title = sandbox_approval_title(&request);
let approve = cx.update(|cx| event_stream.authorize_sandbox(title, request.clone(), cx));
let command = Some(input.command.clone());
let approve =
cx.update(|cx| event_stream.authorize_sandbox(title, command, request.clone(), cx));
if let Err(error) = approve.await {
if want_unsandboxed {
return Ok(format!(
@@ -2490,6 +2492,7 @@ mod tests {
let details =
acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
.expect("legacy allow_fs_write should request sandbox authorization details");
assert_eq!(details.command.as_deref(), Some("echo hi"));
assert!(!details.network);
assert!(details.allow_fs_write_all);
assert!(!details.unsandboxed);
@@ -2586,6 +2589,7 @@ mod tests {
let details =
acp_thread::sandbox_authorization_details_from_meta(&authorization.tool_call.meta)
.expect("unsandboxed should request sandbox authorization details");
assert_eq!(details.command.as_deref(), Some("echo hi"));
assert!(!details.network);
assert!(!details.allow_fs_write_all);
assert!(details.unsandboxed);
@@ -7789,7 +7789,11 @@ impl ThreadView {
details: &SandboxAuthorizationDetails,
cx: &Context<Self>,
) -> AnyElement {
if details.write_paths.is_empty() {
let command = details
.command
.as_deref()
.filter(|command| !command.trim().is_empty());
if command.is_none() && details.write_paths.is_empty() {
return Empty.into_any_element();
}
@@ -7798,63 +7802,75 @@ impl ThreadView {
.contains(tool_call_id);
let mut paths = details.write_paths.clone();
paths.sort();
let has_command = command.is_some();
v_flex()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.child(
h_flex()
.id(("sandbox-authorization-details-header", entry_ix))
.p_1()
.justify_between()
.cursor_pointer()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(
h_flex()
.gap_1()
.child(
Label::new("Write access")
.when_some(command, |this, command| {
this.child(Self::render_sandbox_authorization_command(
entry_ix, command, cx,
))
})
.when(!paths.is_empty(), |this| {
this.child(
h_flex()
.id(("sandbox-authorization-details-header", entry_ix))
.p_1()
.justify_between()
.cursor_pointer()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.when(has_command, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
.child(
h_flex()
.gap_1()
.child(
Label::new("Write access")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("•")
.size(LabelSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(format!(
"{} {}",
paths.len(),
if paths.len() == 1 { "path" } else { "paths" }
))
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(
Label::new("•")
.size(LabelSize::XSmall)
.color(Color::Disabled),
)
.child(
Label::new(format!(
"{} {}",
paths.len(),
if paths.len() == 1 { "path" } else { "paths" }
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
Disclosure::new(("sandbox-authorization-details", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown),
)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
move |this, _event, _window, cx| {
if this
.collapsed_sandbox_authorization_details
.remove(&tool_call_id)
{
cx.notify();
return;
}
),
)
.child(
Disclosure::new(("sandbox-authorization-details", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown),
)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
move |this, _event, _window, cx| {
if this
.collapsed_sandbox_authorization_details
.remove(&tool_call_id)
{
cx.notify();
return;
}
this.collapsed_sandbox_authorization_details
.insert(tool_call_id.clone());
cx.notify();
}
})),
)
.when(is_open, |this| {
this.collapsed_sandbox_authorization_details
.insert(tool_call_id.clone());
cx.notify();
}
})),
)
})
.when(is_open && !paths.is_empty(), |this| {
this.child(
v_flex()
.id(("sandbox-authorization-paths-list", entry_ix))
@@ -7874,6 +7890,49 @@ impl ThreadView {
.into_any_element()
}
fn render_sandbox_authorization_command(entry_ix: usize, command: &str, cx: &App) -> Div {
let group = SharedString::from(format!("sandbox-authorization-command-{entry_ix}"));
let command = SharedString::from(command.to_string());
v_flex()
.group(group.clone())
.relative()
.p_1p5()
.gap_1()
.bg(cx.theme().colors().editor_background)
.child(
Label::new("Command")
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
div()
.id(("sandbox-authorization-command-scroll", entry_ix))
.flex()
.flex_1()
.w_full()
.min_w_0()
.overflow_x_scroll()
.whitespace_nowrap()
.rounded_sm()
.border_1()
.border_color(cx.theme().colors().border)
.p_1()
.child(
Label::new(command.clone())
.buffer_font(cx)
.size(LabelSize::XSmall),
),
)
.child(
div().absolute().top_1().right_1().child(
CopyButton::new("copy-sandbox-authorization-command", command)
.tooltip_label("Copy Command")
.visible_on_hover(group),
),
)
}
fn render_sandbox_authorization_path_row(
&self,
entry_ix: usize,