Use Landlock to restrict bash calls. (#5)
https://docs.kernel.org/userspace-api/landlock.html Reviewed-on: #5 Co-authored-by: Drew Galbraith <drew@tiramisu.one> Co-committed-by: Drew Galbraith <drew@tiramisu.one>
This commit is contained in:
parent
797d7564b7
commit
7efc6705d3
19 changed files with 1315 additions and 238 deletions
|
|
@ -62,6 +62,9 @@ pub(super) fn drain_ui_events(event_rx: &mut mpsc::Receiver<UIEvent>, state: &mu
|
|||
UIEvent::TurnComplete => {
|
||||
debug!("turn complete");
|
||||
}
|
||||
UIEvent::NetworkPolicyChanged(allowed) => {
|
||||
state.network_allowed = allowed;
|
||||
}
|
||||
UIEvent::Error(msg) => {
|
||||
state
|
||||
.messages
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ pub(super) enum LoopControl {
|
|||
ClearHistory,
|
||||
/// The user responded to a tool approval prompt.
|
||||
ToolApproval { tool_use_id: String, approved: bool },
|
||||
/// The user ran `:net on` or `:net off`.
|
||||
SetNetworkPolicy(bool),
|
||||
}
|
||||
|
||||
/// Map a key event to a [`LoopControl`] signal, mutating `state` as a side-effect.
|
||||
|
|
@ -200,6 +202,8 @@ fn execute_command(buf: &str, state: &mut AppState) -> Option<LoopControl> {
|
|||
state.scroll = 0;
|
||||
Some(LoopControl::ClearHistory)
|
||||
}
|
||||
"net on" => Some(LoopControl::SetNetworkPolicy(true)),
|
||||
"net off" => Some(LoopControl::SetNetworkPolicy(false)),
|
||||
other => {
|
||||
state.status_error = Some(format!("Unknown command: {other}"));
|
||||
None
|
||||
|
|
@ -514,6 +518,20 @@ mod tests {
|
|||
assert_eq!(state.status_error.as_deref(), Some("Unknown command: foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_net_on_returns_set_network_policy() {
|
||||
let mut state = AppState::new();
|
||||
let result = execute_command("net on", &mut state);
|
||||
assert!(matches!(result, Some(LoopControl::SetNetworkPolicy(true))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_net_off_returns_set_network_policy() {
|
||||
let mut state = AppState::new();
|
||||
let result = execute_command("net off", &mut state);
|
||||
assert!(matches!(result, Some(LoopControl::SetNetworkPolicy(false))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_error_cleared_on_next_keypress() {
|
||||
let mut state = AppState::new();
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ pub struct AppState {
|
|||
pub status_error: Option<String>,
|
||||
/// A tool approval request waiting for user input (y/n).
|
||||
pub pending_approval: Option<events::PendingApproval>,
|
||||
/// Whether the sandbox is in yolo (unsandboxed) mode.
|
||||
pub sandbox_yolo: bool,
|
||||
/// Whether network access is currently allowed.
|
||||
pub network_allowed: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
|
|
@ -88,6 +92,8 @@ impl AppState {
|
|||
viewport_height: 0,
|
||||
status_error: None,
|
||||
pending_approval: None,
|
||||
sandbox_yolo: false,
|
||||
network_allowed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -145,10 +151,12 @@ pub fn install_panic_hook() {
|
|||
pub async fn run(
|
||||
action_tx: mpsc::Sender<UserAction>,
|
||||
mut event_rx: mpsc::Receiver<UIEvent>,
|
||||
sandbox_yolo: bool,
|
||||
) -> Result<(), TuiError> {
|
||||
install_panic_hook();
|
||||
let mut terminal = init_terminal()?;
|
||||
let mut state = AppState::new();
|
||||
state.sandbox_yolo = sandbox_yolo;
|
||||
let mut event_stream = EventStream::new();
|
||||
|
||||
loop {
|
||||
|
|
@ -199,6 +207,9 @@ pub async fn run(
|
|||
})
|
||||
.await;
|
||||
}
|
||||
Some(input::LoopControl::SetNetworkPolicy(allowed)) => {
|
||||
let _ = action_tx.send(UserAction::SetNetworkPolicy(allowed)).await;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -197,6 +197,24 @@ pub(super) fn render(frame: &mut Frame, state: &AppState) {
|
|||
),
|
||||
};
|
||||
|
||||
// Sandbox indicator: leftmost after mode label.
|
||||
let (sandbox_label, sandbox_style) = if state.sandbox_yolo {
|
||||
(
|
||||
" UNSANDBOXED ",
|
||||
Style::default().bg(Color::Red).fg(Color::White),
|
||||
)
|
||||
} else {
|
||||
let net_label = if state.network_allowed {
|
||||
" NET:ON "
|
||||
} else {
|
||||
" NET:OFF "
|
||||
};
|
||||
(
|
||||
net_label,
|
||||
Style::default().bg(Color::DarkGray).fg(Color::White),
|
||||
)
|
||||
};
|
||||
|
||||
let right_text = if let Some(ref err) = state.status_error {
|
||||
err.clone()
|
||||
} else {
|
||||
|
|
@ -210,12 +228,13 @@ pub(super) fn render(frame: &mut Frame, state: &AppState) {
|
|||
};
|
||||
|
||||
let bar_width = chunks[2].width as usize;
|
||||
let left_len = mode_label.len();
|
||||
let left_len = mode_label.len() + sandbox_label.len();
|
||||
let right_len = right_text.len();
|
||||
let pad = bar_width.saturating_sub(left_len + right_len);
|
||||
|
||||
let status_line = Line::from(vec![
|
||||
Span::styled(mode_label, mode_style),
|
||||
Span::styled(sandbox_label, sandbox_style),
|
||||
Span::raw(" ".repeat(pad)),
|
||||
Span::styled(right_text, right_style),
|
||||
]);
|
||||
|
|
@ -439,4 +458,41 @@ mod tests {
|
|||
"expected tool name in overlay"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_status_bar_shows_net_off() {
|
||||
let backend = TestBackend::new(80, 24);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let state = AppState::new(); // network_allowed defaults to false
|
||||
terminal.draw(|frame| render(frame, &state)).unwrap();
|
||||
let buf = terminal.backend().buffer().clone();
|
||||
let all_text: String = buf
|
||||
.content()
|
||||
.iter()
|
||||
.map(|c| c.symbol().to_string())
|
||||
.collect();
|
||||
assert!(
|
||||
all_text.contains("NET:OFF"),
|
||||
"expected 'NET:OFF' in status bar, got: {all_text:.200}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_status_bar_shows_unsandboxed() {
|
||||
let backend = TestBackend::new(80, 24);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut state = AppState::new();
|
||||
state.sandbox_yolo = true;
|
||||
terminal.draw(|frame| render(frame, &state)).unwrap();
|
||||
let buf = terminal.backend().buffer().clone();
|
||||
let all_text: String = buf
|
||||
.content()
|
||||
.iter()
|
||||
.map(|c| c.symbol().to_string())
|
||||
.collect();
|
||||
assert!(
|
||||
all_text.contains("UNSANDBOXED"),
|
||||
"expected 'UNSANDBOXED' in status bar, got: {all_text:.200}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue