mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Require approval for input to escalated terminals (#40978)
## What changed - Add the `write_stdin_approval` feature flag, disabled by default, to require a fresh approval before sending non-empty input to an escalated unified-exec terminal. - Route these reviews through hooks, Guardian, app-server, and the TUI as `writeStdin` approvals while keeping the parent command item unchanged. - Preserve the terminal environment, launch directory, and escalation state across turns, and revalidate the process identity after approval before writing input. ## Testing - Cover approval routing, policy decisions, process reuse, terminal identity preservation, app-server events, analytics, and TUI rendering. GitOrigin-RevId: 4ba29eaae6208e934737ba078c96e589c7c2164d
This commit is contained in:
1 parent
21ff2e802c
commit
a57b398351
41 files changed
+861
-169
No files matched your search
@@ -40,6 +40,7 @@ use crate::render::renderable::ColumnRenderable;
|
||||
use crate::render::renderable::Renderable;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalKind;
|
||||
use codex_app_server_protocol::FileChangeApprovalDecision;
|
||||
use codex_app_server_protocol::FileSystemAccessMode;
|
||||
use codex_app_server_protocol::FileSystemPath;
|
||||
@@ -79,6 +80,7 @@ pub(crate) enum ApprovalRequest {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ExecApprovalRequest {
|
||||
pub kind: CommandExecutionApprovalKind,
|
||||
pub thread_id: ThreadId,
|
||||
pub thread_label: Option<String>,
|
||||
pub id: String,
|
||||
@@ -250,23 +252,35 @@ impl ApprovalOverlay {
|
||||
list_keymap: &ListKeymap,
|
||||
) -> (Vec<ApprovalOption>, SelectionViewParams) {
|
||||
let (options, title) = match request {
|
||||
ApprovalRequest::Exec(request) => (
|
||||
exec_options(
|
||||
&request.available_decisions,
|
||||
request.network_approval_context.as_ref(),
|
||||
request.additional_permissions.as_ref(),
|
||||
approval_keymap,
|
||||
),
|
||||
request.network_approval_context.as_ref().map_or_else(
|
||||
|| "Would you like to run the following command?".to_string(),
|
||||
|network_approval_context| {
|
||||
format!(
|
||||
"Do you want to approve network access to \"{}\"?",
|
||||
network_approval_context.host
|
||||
)
|
||||
},
|
||||
),
|
||||
),
|
||||
ApprovalRequest::Exec(request) => {
|
||||
let title = if request.kind == CommandExecutionApprovalKind::WriteStdin {
|
||||
request.command.get(2).map_or_else(
|
||||
|| "Would you like to send input to the existing terminal?".to_string(),
|
||||
|process_id| {
|
||||
format!("Would you like to send input to terminal {process_id}?")
|
||||
},
|
||||
)
|
||||
} else {
|
||||
request.network_approval_context.as_ref().map_or_else(
|
||||
|| "Would you like to run the following command?".to_string(),
|
||||
|network_approval_context| {
|
||||
format!(
|
||||
"Do you want to approve network access to \"{}\"?",
|
||||
network_approval_context.host
|
||||
)
|
||||
},
|
||||
)
|
||||
};
|
||||
(
|
||||
exec_options(
|
||||
&request.available_decisions,
|
||||
request.network_approval_context.as_ref(),
|
||||
request.additional_permissions.as_ref(),
|
||||
approval_keymap,
|
||||
),
|
||||
title,
|
||||
)
|
||||
}
|
||||
ApprovalRequest::Permissions(_) => (
|
||||
permissions_options(approval_keymap),
|
||||
"Would you like to grant these permissions?".to_string(),
|
||||
@@ -704,13 +718,18 @@ fn build_header(request: &ApprovalRequest) -> Box<dyn Renderable> {
|
||||
]));
|
||||
header.push(Line::from(""));
|
||||
}
|
||||
let full_cmd = strip_bash_lc_and_escape(&request.command);
|
||||
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
|
||||
if let Some(first) = full_cmd_lines.first_mut() {
|
||||
first.spans.insert(0, Span::from("$ "));
|
||||
}
|
||||
if request.network_approval_context.is_none() {
|
||||
header.extend(full_cmd_lines);
|
||||
if request.kind == CommandExecutionApprovalKind::WriteStdin {
|
||||
let input = request.command.last().map_or("", String::as_str);
|
||||
header.push(vec!["Input: ".into(), format!("{input:?}").into()].into());
|
||||
} else {
|
||||
let full_cmd = strip_bash_lc_and_escape(&request.command);
|
||||
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
|
||||
if let Some(first) = full_cmd_lines.first_mut() {
|
||||
first.spans.insert(0, Span::from("$ "));
|
||||
}
|
||||
if request.network_approval_context.is_none() {
|
||||
header.extend(full_cmd_lines);
|
||||
}
|
||||
}
|
||||
Box::new(Paragraph::new(header).wrap(Wrap { trim: false }))
|
||||
}
|
||||
@@ -1187,6 +1206,7 @@ mod tests {
|
||||
|
||||
fn make_exec_request() -> ApprovalRequest {
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
@@ -1380,6 +1400,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx);
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
@@ -1424,6 +1445,7 @@ mod tests {
|
||||
};
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
@@ -1509,6 +1531,7 @@ mod tests {
|
||||
let thread_id = ThreadId::new();
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id,
|
||||
thread_label: Some("Robie [explorer]".to_string()),
|
||||
id: "test".to_string(),
|
||||
@@ -1544,6 +1567,7 @@ mod tests {
|
||||
keymap.approval.open_thread = vec![key_hint::plain(KeyCode::Char('x'))];
|
||||
let mut view = make_overlay_with_keymap(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id,
|
||||
thread_label: Some("Robie [explorer]".to_string()),
|
||||
id: "test".to_string(),
|
||||
@@ -1583,6 +1607,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx);
|
||||
let view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: Some("Robie [explorer]".to_string()),
|
||||
id: "test".to_string(),
|
||||
@@ -1612,6 +1637,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx);
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
@@ -1665,6 +1691,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx);
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".to_string(),
|
||||
@@ -1705,6 +1732,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx);
|
||||
let command = vec!["echo".into(), "hello".into(), "world".into()];
|
||||
let exec_request = ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
@@ -2006,6 +2034,7 @@ mod tests {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
@@ -2063,6 +2092,7 @@ mod tests {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
@@ -2188,6 +2218,7 @@ mod tests {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
@@ -2325,6 +2356,7 @@ mod tests {
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut view = make_overlay(
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
|
||||
@@ -2011,6 +2011,7 @@ mod tests {
|
||||
|
||||
fn exec_request() -> ApprovalRequest {
|
||||
ApprovalRequest::Exec(ExecApprovalRequest {
|
||||
kind: Default::default(),
|
||||
thread_id: codex_protocol::ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "1".to_string(),
|
||||
|
||||
Reference in new issue
Block a user