Reject oversized reviewed terminal input (#41159)

## Why

Reviewing a shortened `write_stdin` action while sending the complete input could allow unreviewed bytes to reach an escalated terminal.

## What changed

- Require the fully formatted action and approval reason to fit within 8,000 bytes.
- Reject truncated or oversized actions before requesting approval or writing any bytes to the terminal.
- Document the reviewed stdin size limit in the app-server protocol.

## Testing

Add an integration test that uses JSON escaping to push a formatted action over the limit and verifies that it is neither offered for approval nor executed, while a subsequent valid write still succeeds.

GitOrigin-RevId: f6c96a78a65a136366c8119a7d242dfc2ec05fe6
This commit is contained in:
jif
2026-08-27 14:54:13 +00:00
committed by copyberry
parent 453a9bcc69
commit bce96bcb43
6 changed files with 147 additions and 4 deletions
+1 -2
View File
@@ -244,8 +244,7 @@ impl GuardianRejectionCircuitBreaker {
}
}
#[cfg(test)]
use approval_request::format_guardian_action_pretty;
pub(crate) use approval_request::format_guardian_action_pretty;
#[cfg(test)]
use approval_request::guardian_assessment_action;
#[cfg(test)]
+3 -1
View File
@@ -266,7 +266,9 @@ impl ApprovalAction {
}
}
fn into_guardian_request(self) -> std::io::Result<crate::guardian::GuardianApprovalRequest> {
pub(crate) fn into_guardian_request(
self,
) -> std::io::Result<crate::guardian::GuardianApprovalRequest> {
Ok(match self {
Self::ExecCommand {
id,
@@ -798,6 +798,22 @@ impl UnifiedExecProcessManager {
};
let _interaction_guard = locked_process.interaction_lock().lock_owned().await;
if let Some(approval) = approval {
let approval_reason = "Send input to an existing escalated terminal. The cwd is its launch directory; the terminal's current directory and state may have changed.".to_string();
let reviewed = crate::guardian::format_guardian_action_pretty(
&approval.clone().into_guardian_request().map_err(|err| {
UnifiedExecError::StdinApproval(ToolError::Rejected(err.to_string()))
})?,
)
.map_err(|err| UnifiedExecError::StdinApproval(ToolError::Rejected(err.to_string())))?;
// Bound the entire serialized action plus its reason, including JSON
// escaping. Reject, never execute an unreviewed tail.
if reviewed.truncated
|| reviewed.text.len().saturating_add(approval_reason.len()) > 8_000
{
return Err(UnifiedExecError::StdinApproval(ToolError::Rejected(
"terminal input and permission details are too large to review safely; use a smaller input or start a new terminal with fewer grants".to_string(),
)));
}
let strict_auto_review = context
.session
.active_turn_context_and_strict_auto_review()
@@ -809,7 +825,7 @@ impl UnifiedExecProcessManager {
call_id: context.call_id.clone(),
tool_name: ToolName::plain("write_stdin"),
strict_auto_review,
approval_reason: Some("Send input to an existing escalated terminal. The cwd is its launch directory; the terminal's current directory and state may have changed.".to_string()),
approval_reason: Some(approval_reason),
retry_reason: None,
network_approval_context: None,
};