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
|
|
@ -3,17 +3,19 @@
|
|||
//! All tools implement the [`Tool`] trait. The [`ToolRegistry`] collects them
|
||||
//! and provides lookup by name plus generation of [`ToolDefinition`]s for the
|
||||
//! model provider.
|
||||
//!
|
||||
//! Tools execute through [`crate::sandbox::Sandbox`] -- they must never use
|
||||
//! `std::fs` or `std::process::Command` directly.
|
||||
|
||||
mod list_directory;
|
||||
mod read_file;
|
||||
mod shell_exec;
|
||||
mod write_file;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::core::types::ToolDefinition;
|
||||
use crate::sandbox::Sandbox;
|
||||
|
||||
/// The output of a tool execution.
|
||||
#[derive(Debug)]
|
||||
|
|
@ -35,6 +37,10 @@ pub enum RiskLevel {
|
|||
|
||||
/// A tool that the model can invoke.
|
||||
///
|
||||
/// All file I/O and process spawning must go through the [`Sandbox`] passed
|
||||
/// to [`Tool::execute`]. Tools must never use `std::fs` or
|
||||
/// `std::process::Command` directly.
|
||||
///
|
||||
/// The `execute` method is async so that tool implementations can use
|
||||
/// `tokio::fs` and `tokio::process` without blocking a Tokio worker thread.
|
||||
/// `#[async_trait]` desugars the async fn to a boxed future, which is required
|
||||
|
|
@ -49,71 +55,26 @@ pub trait Tool: Send + Sync {
|
|||
fn input_schema(&self) -> serde_json::Value;
|
||||
/// The risk level of this tool.
|
||||
fn risk_level(&self) -> RiskLevel;
|
||||
/// Execute the tool with the given input, confined to `working_dir`.
|
||||
/// Execute the tool with the given input, confined by `sandbox`.
|
||||
async fn execute(
|
||||
&self,
|
||||
input: &serde_json::Value,
|
||||
working_dir: &Path,
|
||||
sandbox: &Sandbox,
|
||||
) -> Result<ToolOutput, ToolError>;
|
||||
}
|
||||
|
||||
/// Errors from tool execution.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ToolError {
|
||||
/// The requested path escapes the working directory.
|
||||
#[error("path escapes working directory: {0}")]
|
||||
PathEscape(PathBuf),
|
||||
/// An I/O error during tool execution.
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// A required input field is missing or has the wrong type.
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
}
|
||||
|
||||
/// Validate that `requested` resolves to a path inside `working_dir`.
|
||||
///
|
||||
/// Joins `working_dir` with `requested`, canonicalizes the result (resolving
|
||||
/// symlinks and `..` components), and checks that the canonical path starts
|
||||
/// with the canonical working directory.
|
||||
///
|
||||
/// Returns the canonical path on success, or [`ToolError::PathEscape`] if the
|
||||
/// path would escape the working directory.
|
||||
pub fn validate_path(working_dir: &Path, requested: &str) -> Result<PathBuf, ToolError> {
|
||||
let candidate = if Path::new(requested).is_absolute() {
|
||||
PathBuf::from(requested)
|
||||
} else {
|
||||
working_dir.join(requested)
|
||||
};
|
||||
|
||||
// For paths that don't exist yet (e.g. write_file creating a new file),
|
||||
// canonicalize the parent directory and append the filename.
|
||||
let canonical = if candidate.exists() {
|
||||
candidate
|
||||
.canonicalize()
|
||||
.map_err(|_| ToolError::PathEscape(candidate.clone()))?
|
||||
} else {
|
||||
let parent = candidate
|
||||
.parent()
|
||||
.ok_or_else(|| ToolError::PathEscape(candidate.clone()))?;
|
||||
let file_name = candidate
|
||||
.file_name()
|
||||
.ok_or_else(|| ToolError::PathEscape(candidate.clone()))?;
|
||||
let canonical_parent = parent
|
||||
.canonicalize()
|
||||
.map_err(|_| ToolError::PathEscape(candidate.clone()))?;
|
||||
canonical_parent.join(file_name)
|
||||
};
|
||||
|
||||
let canonical_root = working_dir
|
||||
.canonicalize()
|
||||
.map_err(|_| ToolError::PathEscape(candidate.clone()))?;
|
||||
|
||||
if canonical.starts_with(&canonical_root) {
|
||||
Ok(canonical)
|
||||
} else {
|
||||
Err(ToolError::PathEscape(candidate))
|
||||
}
|
||||
/// A sandbox error during tool execution.
|
||||
#[error("sandbox error: {0}")]
|
||||
Sandbox(#[from] crate::sandbox::SandboxError),
|
||||
}
|
||||
|
||||
/// Collection of available tools with name-based lookup.
|
||||
|
|
@ -161,15 +122,16 @@ impl ToolRegistry {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use crate::sandbox::test_sandbox;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn validate_path_allows_subpath() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let sub = dir.path().join("sub");
|
||||
fs::create_dir(&sub).unwrap();
|
||||
let result = validate_path(dir.path(), "sub");
|
||||
std::fs::create_dir(&sub).unwrap();
|
||||
let sandbox = test_sandbox(dir.path());
|
||||
let result = sandbox.validate_path("sub");
|
||||
assert!(result.is_ok());
|
||||
assert!(
|
||||
result
|
||||
|
|
@ -181,22 +143,24 @@ mod tests {
|
|||
#[test]
|
||||
fn validate_path_rejects_traversal() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let result = validate_path(dir.path(), "../../../etc/passwd");
|
||||
let sandbox = test_sandbox(dir.path());
|
||||
let result = sandbox.validate_path("../../../etc/passwd");
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(result, Err(ToolError::PathEscape(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_path_rejects_absolute_outside() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let result = validate_path(dir.path(), "/etc/passwd");
|
||||
let sandbox = test_sandbox(dir.path());
|
||||
let result = sandbox.validate_path("/etc/passwd");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_path_allows_new_file_in_working_dir() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let result = validate_path(dir.path(), "new_file.txt");
|
||||
let sandbox = test_sandbox(dir.path());
|
||||
let result = sandbox.validate_path("new_file.txt");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue