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:
Dylan Hurd authored and copyberry committed 2026-08-26 21:39:05 +00:00
1 parent 21ff2e802c
commit a57b398351
41 files changed
+861 -169

No files matched your search

@@ -430,6 +430,7 @@ async fn exec_approval_emits_proposed_command_and_decision_history() {
// Trigger an exec approval request with a short, single-line command
let ev = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-short".into(),
approval_id: Some("call-short".into()),
turn_id: "turn-short".into(),
@@ -522,6 +523,7 @@ async fn exec_approval_uses_approval_id_when_present() {
&mut chat,
"sub-short",
ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-parent".into(),
approval_id: Some("approval-subcommand".into()),
turn_id: "turn-short".into(),
@@ -566,6 +568,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
// Multiline command: modal should show full command, history records decision only
let ev_multi = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-multi".into(),
approval_id: Some("call-multi".into()),
turn_id: "turn-multi".into(),
@@ -620,6 +623,7 @@ async fn exec_approval_decision_truncates_multiline_and_long_commands() {
// Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...
let long = format!("echo {}", "a".repeat(200));
let ev_long = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-long".into(),
approval_id: Some("call-long".into()),
turn_id: "turn-long".into(),
@@ -1552,6 +1556,7 @@ async fn approval_modal_exec_snapshot() -> anyhow::Result<()> {
.set(AskForApproval::OnRequest.to_core())?;
// Inject an exec approval request to display the approval modal.
let ev = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-approve-cmd".into(),
approval_id: Some("call-approve-cmd".into()),
turn_id: "turn-approve-cmd".into(),
@@ -1610,6 +1615,7 @@ async fn approval_modal_exec_without_reason_snapshot() -> anyhow::Result<()> {
.set(AskForApproval::OnRequest.to_core())?;
let ev = ExecApprovalRequestEvent {
kind: Default::default(),
call_id: "call-approve-cmd-noreason".into(),
approval_id: Some("call-approve-cmd-noreason".into()),
turn_id: "turn-approve-cmd-noreason".into(),
@@ -1656,6 +1662,7 @@ async fn approval_modal_exec_multiline_prefix_hides_execpolicy_option_snapshot()
let script = "python - <<'PY'\nprint('hello')\nPY".to_string();
let command = vec!["bash".into(), "-lc".into(), script];
let ev = ExecApprovalRequestEvent {
kind: Default::default(),
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(),