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:
Drew 2026-03-02 03:51:46 +00:00 committed by Drew
parent 797d7564b7
commit 7efc6705d3
19 changed files with 1315 additions and 238 deletions

View file

@ -7,6 +7,7 @@ use crate::core::types::{
ContentBlock, ConversationMessage, Role, StreamEvent, ToolDefinition, UIEvent, UserAction,
};
use crate::provider::ModelProvider;
use crate::sandbox::Sandbox;
use crate::tools::{RiskLevel, ToolOutput, ToolRegistry};
/// Accumulates data for a single tool-use block while it is being streamed.
@ -106,7 +107,7 @@ pub struct Orchestrator<P> {
history: ConversationHistory,
provider: P,
tool_registry: ToolRegistry,
working_dir: std::path::PathBuf,
sandbox: Sandbox,
action_rx: mpsc::Receiver<UserAction>,
event_tx: mpsc::Sender<UIEvent>,
/// Messages typed by the user while an approval prompt is open. They are
@ -119,7 +120,7 @@ impl<P: ModelProvider> Orchestrator<P> {
pub fn new(
provider: P,
tool_registry: ToolRegistry,
working_dir: std::path::PathBuf,
sandbox: Sandbox,
action_rx: mpsc::Receiver<UserAction>,
event_tx: mpsc::Sender<UIEvent>,
) -> Self {
@ -127,7 +128,7 @@ impl<P: ModelProvider> Orchestrator<P> {
history: ConversationHistory::new(),
provider,
tool_registry,
working_dir,
sandbox,
action_rx,
event_tx,
queued_messages: Vec::new(),
@ -340,7 +341,7 @@ impl<P: ModelProvider> Orchestrator<P> {
// Re-fetch tool for execution (borrow was released above).
let tool = self.tool_registry.get(tool_name).unwrap();
match tool.execute(input, &self.working_dir).await {
match tool.execute(input, &self.sandbox).await {
Ok(output) => {
let _ = self
.event_tx
@ -409,6 +410,14 @@ impl<P: ModelProvider> Orchestrator<P> {
// not in the main action loop. If one arrives here it's stale.
UserAction::ToolApprovalResponse { .. } => {}
UserAction::SetNetworkPolicy(allowed) => {
self.sandbox.set_network_allowed(allowed);
let _ = self
.event_tx
.send(UIEvent::NetworkPolicyChanged(allowed))
.await;
}
UserAction::SendMessage(text) => {
self.history
.push(ConversationMessage::text(Role::User, text));
@ -467,10 +476,17 @@ mod tests {
action_rx: mpsc::Receiver<UserAction>,
event_tx: mpsc::Sender<UIEvent>,
) -> Orchestrator<P> {
use crate::sandbox::policy::SandboxPolicy;
use crate::sandbox::{EnforcementMode, Sandbox};
let sandbox = Sandbox::new(
SandboxPolicy::for_project(std::path::PathBuf::from("/tmp")),
std::path::PathBuf::from("/tmp"),
EnforcementMode::Yolo,
);
Orchestrator::new(
provider,
ToolRegistry::empty(),
std::path::PathBuf::from("/tmp"),
sandbox,
action_rx,
event_tx,
)
@ -717,12 +733,19 @@ mod tests {
let (event_tx, mut event_rx) = mpsc::channel::<UIEvent>(32);
// Use a real ToolRegistry so read_file works.
use crate::sandbox::policy::SandboxPolicy;
use crate::sandbox::{EnforcementMode, Sandbox};
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
let sandbox = Sandbox::new(
SandboxPolicy::for_project(dir.path().to_path_buf()),
dir.path().to_path_buf(),
EnforcementMode::Yolo,
);
let orch = Orchestrator::new(
MultiCallMock { turns },
ToolRegistry::default_tools(),
dir.path().to_path_buf(),
sandbox,
action_rx,
event_tx,
);
@ -873,14 +896,7 @@ mod tests {
let (action_tx, action_rx) = mpsc::channel::<UserAction>(16);
let (event_tx, mut event_rx) = mpsc::channel::<UIEvent>(64);
let dir = tempfile::TempDir::new().unwrap();
let orch = Orchestrator::new(
MultiCallMock { turns },
ToolRegistry::default_tools(),
dir.path().to_path_buf(),
action_rx,
event_tx,
);
let orch = test_orchestrator(MultiCallMock { turns }, action_rx, event_tx);
let handle = tokio::spawn(orch.run());
// Start turn 1 -- orchestrator will block on approval.
@ -927,4 +943,45 @@ mod tests {
action_tx.send(UserAction::Quit).await.unwrap();
handle.await.unwrap();
}
// -- network policy toggle ------------------------------------------------
/// SetNetworkPolicy sends a NetworkPolicyChanged event back to the TUI.
#[tokio::test]
async fn set_network_policy_sends_event() {
// Provider that returns immediately to avoid blocking.
struct NeverCalledProvider;
impl ModelProvider for NeverCalledProvider {
fn stream<'a>(
&'a self,
_messages: &'a [ConversationMessage],
_tools: &'a [ToolDefinition],
) -> impl Stream<Item = StreamEvent> + Send + 'a {
futures::stream::empty()
}
}
let (action_tx, action_rx) = mpsc::channel::<UserAction>(8);
let (event_tx, mut event_rx) = mpsc::channel::<UIEvent>(8);
let orch = test_orchestrator(NeverCalledProvider, action_rx, event_tx);
let handle = tokio::spawn(orch.run());
action_tx
.send(UserAction::SetNetworkPolicy(true))
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let mut found = false;
while let Ok(ev) = event_rx.try_recv() {
if matches!(ev, UIEvent::NetworkPolicyChanged(true)) {
found = true;
}
}
assert!(found, "expected NetworkPolicyChanged(true) event");
action_tx.send(UserAction::Quit).await.unwrap();
handle.await.unwrap();
}
}