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:
1 parent
37f4c1f69d
commit
54c46bb34f
4 files changed
+121
-53
No files matched your search
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user