core-agent-ide/codex-rs/tui/src/test_backend.rs
gt-oai 9352c6b235
feat: Constrain values for approval_policy (#7778)
Constrain `approval_policy` through new `admin_policy` config.

This PR will:
1. Add a `admin_policy` section to config, with a single field (for now)
`allowed_approval_policies`. This list constrains the set of
user-settable `approval_policy`s.
2. Introduce a new `Constrained<T>` type, which combines a current value
and a validator function. The validator function ensures disallowed
values are not set.
3. Change the type of `approval_policy` on `Config` and
`SessionConfiguration` from `AskForApproval` to
`Constrained<AskForApproval>`. The validator function is set by the
values passed into `allowed_approval_policies`.
4. `GenericDisplayRow`: add a `disabled_reason: Option<String>`. When
set, it disables selection of the value and indicates as such in the
menu. This also makes it unselectable with arrow keys or numbers. This
is used in the `/approvals` menu.

Follow ups are:
1. Do the same thing to `sandbox_policy`.
2. Propagate the allowed set of values through app-server for the
extension (though already this should prevent app-server from setting
this values, it's just that we want to disable UI elements that are
unsettable).

Happy to split this PR up if you prefer, into the logical numbered areas
above. Especially if there are parts we want to gavel on separately
(e.g. admin_policy).

Disabled full access:
<img width="1680" height="380" alt="image"
src="https://github.com/user-attachments/assets/1fb61c8c-1fcb-4dc4-8355-2293edb52ba0"
/>

Disabled `--yolo` on startup:
<img width="749" height="76" alt="image"
src="https://github.com/user-attachments/assets/0a1211a0-6eb1-40d6-a1d7-439c41e94ddb"
/>

CODEX-4087
2025-12-17 16:19:27 +00:00

125 lines
3.5 KiB
Rust

use std::fmt::{self};
use std::io::Write;
use std::io::{self};
use ratatui::prelude::CrosstermBackend;
use ratatui::backend::Backend;
use ratatui::backend::ClearType;
use ratatui::backend::WindowSize;
use ratatui::buffer::Cell;
use ratatui::layout::Position;
use ratatui::layout::Size;
/// This wraps a CrosstermBackend and a vt100::Parser to mock
/// a "real" terminal.
///
/// Importantly, this wrapper avoids calling any crossterm methods
/// which write to stdout regardless of the writer. This includes:
/// - getting the terminal size
/// - getting the cursor position
pub struct VT100Backend {
crossterm_backend: CrosstermBackend<vt100::Parser>,
}
impl VT100Backend {
/// Creates a new `TestBackend` with the specified width and height.
pub fn new(width: u16, height: u16) -> Self {
crossterm::style::force_color_output(true);
Self {
crossterm_backend: CrosstermBackend::new(vt100::Parser::new(height, width, 0)),
}
}
pub fn vt100(&self) -> &vt100::Parser {
self.crossterm_backend.writer()
}
}
impl Write for VT100Backend {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.crossterm_backend.writer_mut().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.crossterm_backend.writer_mut().flush()
}
}
impl fmt::Display for VT100Backend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.crossterm_backend.writer().screen().contents())
}
}
impl Backend for VT100Backend {
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
self.crossterm_backend.draw(content)?;
Ok(())
}
fn hide_cursor(&mut self) -> io::Result<()> {
self.crossterm_backend.hide_cursor()?;
Ok(())
}
fn show_cursor(&mut self) -> io::Result<()> {
self.crossterm_backend.show_cursor()?;
Ok(())
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
Ok(self.vt100().screen().cursor_position().into())
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
self.crossterm_backend.set_cursor_position(position)
}
fn clear(&mut self) -> io::Result<()> {
self.crossterm_backend.clear()
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
self.crossterm_backend.clear_region(clear_type)
}
fn append_lines(&mut self, line_count: u16) -> io::Result<()> {
self.crossterm_backend.append_lines(line_count)
}
fn size(&self) -> io::Result<Size> {
let (rows, cols) = self.vt100().screen().size();
Ok(Size::new(cols, rows))
}
fn window_size(&mut self) -> io::Result<WindowSize> {
Ok(WindowSize {
columns_rows: self.vt100().screen().size().into(),
// Arbitrary size, we don't rely on this in testing.
pixels: Size {
width: 640,
height: 480,
},
})
}
fn flush(&mut self) -> io::Result<()> {
self.crossterm_backend.writer_mut().flush()
}
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, scroll_by: u16) -> io::Result<()> {
self.crossterm_backend.scroll_region_up(region, scroll_by)
}
fn scroll_region_down(
&mut self,
region: std::ops::Range<u16>,
scroll_by: u16,
) -> io::Result<()> {
self.crossterm_backend.scroll_region_down(region, scroll_by)
}
}