context_harness/agent_runtime/
terminal.rs1use super::policy::{ApprovalHandler, ApprovalRequest};
3use anyhow::Context;
4use async_trait::async_trait;
5
6pub(super) struct TerminalApprovals;
7#[async_trait]
8impl ApprovalHandler for TerminalApprovals {
9 async fn approve(&self, request: &ApprovalRequest) -> bool {
10 match read_approval(request).await {
11 Ok(granted) => granted,
12 Err(error) => {
13 eprintln!("Agent approval unavailable; denying invocation: {error:#}");
14 false
15 }
16 }
17 }
18}
19
20#[cfg(unix)]
21async fn read_approval(request: &ApprovalRequest) -> anyhow::Result<bool> {
22 use std::{fs::OpenOptions, io::IsTerminal, os::unix::fs::OpenOptionsExt};
23
24 if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
25 eprintln!("Agent approval unavailable; stdin and stderr must both be terminals.");
26 return Ok(false);
27 }
28 let tty = OpenOptions::new()
29 .read(true)
30 .write(true)
31 .custom_flags(libc::O_NONBLOCK)
32 .open("/dev/tty")
33 .context("opening /dev/tty")?;
34 read_from_tty(tty, request).await
35}
36
37#[cfg(unix)]
38async fn read_from_tty(mut tty: std::fs::File, request: &ApprovalRequest) -> anyhow::Result<bool> {
39 use std::{
40 io::{Read, Write},
41 os::fd::AsRawFd,
42 };
43 if unsafe { libc::tcflush(tty.as_raw_fd(), libc::TCIFLUSH) } != 0 {
46 return Err(std::io::Error::last_os_error()).context("flushing queued terminal input");
47 }
48 let details = serde_json::json!({"run":request.run_id,"call":request.call_id,
50 "tool":request.tool,"capabilities":request.capabilities,"arguments":request.arguments});
51 let details: String = serde_json::to_string_pretty(&details)?
52 .chars()
53 .map(|c| {
54 if c == '\n' || (c.is_ascii() && !c.is_control()) {
55 c.to_string()
56 } else {
57 c.escape_default().to_string()
58 }
59 })
60 .collect();
61 if details.len() > 32 * 1024 {
63 eprintln!("Agent approval unavailable; invocation details exceed the display limit.");
64 return Ok(false);
65 }
66 let warning = if request.tool == "process.exec"
67 || request.tool.starts_with("runtime.mcp.start.")
68 || request.tool.starts_with("mcp.")
69 {
70 "\nThis process is not sandboxed: it can access files, credentials, network, and child processes with your user permissions."
71 } else {
72 ""
73 };
74 eprintln!("\nAgent requests approval:\n{details}{warning}\nAllow this invocation once? Type yes to approve; anything else denies:");
75 std::io::stderr().flush()?;
76 let mut answer = Vec::new();
77 loop {
78 let mut buf = [0u8; 128];
79 match tty.read(&mut buf) {
80 Ok(0) => {
81 eprintln!("Agent approval denied; terminal input closed before a response.");
82 return Ok(false);
83 }
84 Ok(n) => {
85 answer.extend_from_slice(&buf[..n]);
86 if answer.len() > 128 {
87 return Ok(false);
88 }
89 if answer.contains(&b'\n') {
90 return Ok(answer == b"yes\n" || answer == b"yes\r\n");
91 }
92 }
93 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
94 tokio::time::sleep(std::time::Duration::from_millis(25)).await;
99 }
100 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
101 Err(error) => return Err(error).context("reading terminal approval response"),
102 }
103 }
104}
105#[cfg(not(unix))]
106async fn read_approval(_: &ApprovalRequest) -> anyhow::Result<bool> {
107 Ok(false)
109}
110
111#[cfg(all(test, unix))]
112mod tests {
113 use super::*;
114 use std::{fs::File, io::Write, os::fd::FromRawFd, time::Duration};
115 #[tokio::test]
116 async fn queued_answers_do_not_approve_a_new_invocation() {
117 let (mut master, mut slave) = (-1, -1);
118 assert_eq!(
119 unsafe {
120 libc::openpty(
121 &mut master,
122 &mut slave,
123 std::ptr::null_mut(),
124 std::ptr::null_mut(),
125 std::ptr::null_mut(),
126 )
127 },
128 0
129 );
130 let mut master = unsafe { File::from_raw_fd(master) };
131 let tty = unsafe { File::from_raw_fd(slave) };
132 assert_ne!(
133 unsafe { libc::fcntl(slave, libc::F_SETFL, libc::O_NONBLOCK) },
134 -1
135 );
136 master.write_all(b"yes\nyes\n").unwrap();
137 tokio::time::sleep(Duration::from_millis(20)).await;
139 let request = ApprovalRequest {
140 run_id: "run".into(),
141 call_id: "call".into(),
142 tool: "workspace.patch".into(),
143 capabilities: vec![],
144 arguments: serde_json::json!({}),
145 };
146 let future = read_from_tty(tty, &request);
147 tokio::pin!(future);
148 assert!(tokio::time::timeout(Duration::from_millis(30), &mut future)
149 .await
150 .is_err());
151 master.write_all(b"yes\n").unwrap();
152 assert!(tokio::time::timeout(Duration::from_secs(1), future)
153 .await
154 .unwrap()
155 .unwrap());
156 }
157}