fix(channels): execute tool calls in channel runtime (#302)
* fix(channels): execute tool calls in channel runtime (#302) * chore(fmt): align repo formatting with rustfmt 1.92
This commit is contained in:
parent
efabe9703f
commit
9d29f30a31
17 changed files with 483 additions and 127 deletions
|
|
@ -88,7 +88,12 @@ impl AuditEvent {
|
|||
}
|
||||
|
||||
/// Set the actor
|
||||
pub fn with_actor(mut self, channel: String, user_id: Option<String>, username: Option<String>) -> Self {
|
||||
pub fn with_actor(
|
||||
mut self,
|
||||
channel: String,
|
||||
user_id: Option<String>,
|
||||
username: Option<String>,
|
||||
) -> Self {
|
||||
self.actor = Some(Actor {
|
||||
channel,
|
||||
user_id,
|
||||
|
|
@ -98,7 +103,13 @@ impl AuditEvent {
|
|||
}
|
||||
|
||||
/// Set the action
|
||||
pub fn with_action(mut self, command: String, risk_level: String, approved: bool, allowed: bool) -> Self {
|
||||
pub fn with_action(
|
||||
mut self,
|
||||
command: String,
|
||||
risk_level: String,
|
||||
approved: bool,
|
||||
allowed: bool,
|
||||
) -> Self {
|
||||
self.action = Some(Action {
|
||||
command: Some(command),
|
||||
risk_level: Some(risk_level),
|
||||
|
|
@ -109,7 +120,13 @@ impl AuditEvent {
|
|||
}
|
||||
|
||||
/// Set the result
|
||||
pub fn with_result(mut self, success: bool, exit_code: Option<i32>, duration_ms: u64, error: Option<String>) -> Self {
|
||||
pub fn with_result(
|
||||
mut self,
|
||||
success: bool,
|
||||
exit_code: Option<i32>,
|
||||
duration_ms: u64,
|
||||
error: Option<String>,
|
||||
) -> Self {
|
||||
self.result = Some(ExecutionResult {
|
||||
success,
|
||||
exit_code,
|
||||
|
|
@ -179,7 +196,12 @@ impl AuditLogger {
|
|||
) -> Result<()> {
|
||||
let event = AuditEvent::new(AuditEventType::CommandExecution)
|
||||
.with_actor(channel.to_string(), None, None)
|
||||
.with_action(command.to_string(), risk_level.to_string(), approved, allowed)
|
||||
.with_action(
|
||||
command.to_string(),
|
||||
risk_level.to_string(),
|
||||
approved,
|
||||
allowed,
|
||||
)
|
||||
.with_result(success, None, duration_ms, None);
|
||||
|
||||
self.log(&event)
|
||||
|
|
@ -224,8 +246,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn audit_event_with_actor() {
|
||||
let event = AuditEvent::new(AuditEventType::CommandExecution)
|
||||
.with_actor("telegram".to_string(), Some("123".to_string()), Some("@alice".to_string()));
|
||||
let event = AuditEvent::new(AuditEventType::CommandExecution).with_actor(
|
||||
"telegram".to_string(),
|
||||
Some("123".to_string()),
|
||||
Some("@alice".to_string()),
|
||||
);
|
||||
|
||||
assert!(event.actor.is_some());
|
||||
let actor = event.actor.as_ref().unwrap();
|
||||
|
|
@ -236,8 +261,12 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn audit_event_with_action() {
|
||||
let event = AuditEvent::new(AuditEventType::CommandExecution)
|
||||
.with_action("ls -la".to_string(), "low".to_string(), false, true);
|
||||
let event = AuditEvent::new(AuditEventType::CommandExecution).with_action(
|
||||
"ls -la".to_string(),
|
||||
"low".to_string(),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(event.action.is_some());
|
||||
let action = event.action.as_ref().unwrap();
|
||||
|
|
|
|||
|
|
@ -35,14 +35,23 @@ impl BubblewrapSandbox {
|
|||
impl Sandbox for BubblewrapSandbox {
|
||||
fn wrap_command(&self, cmd: &mut Command) -> std::io::Result<()> {
|
||||
let program = cmd.get_program().to_string_lossy().to_string();
|
||||
let args: Vec<String> = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect();
|
||||
let args: Vec<String> = cmd
|
||||
.get_args()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let mut bwrap_cmd = Command::new("bwrap");
|
||||
bwrap_cmd.args([
|
||||
"--ro-bind", "/usr", "/usr",
|
||||
"--dev", "/dev",
|
||||
"--proc", "/proc",
|
||||
"--bind", "/tmp", "/tmp",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--bind",
|
||||
"/tmp",
|
||||
"/tmp",
|
||||
"--unshare-all",
|
||||
"--die-with-parent",
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc<dyn Sandbox> {
|
|||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("Landlock requested but not available, falling back to application-layer");
|
||||
tracing::warn!(
|
||||
"Landlock requested but not available, falling back to application-layer"
|
||||
);
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Firejail => {
|
||||
|
|
@ -35,7 +37,9 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc<dyn Sandbox> {
|
|||
return Arc::new(sandbox);
|
||||
}
|
||||
}
|
||||
tracing::warn!("Firejail requested but not available, falling back to application-layer");
|
||||
tracing::warn!(
|
||||
"Firejail requested but not available, falling back to application-layer"
|
||||
);
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Bubblewrap => {
|
||||
|
|
@ -48,7 +52,9 @@ pub fn create_sandbox(config: &SecurityConfig) -> Arc<dyn Sandbox> {
|
|||
}
|
||||
}
|
||||
}
|
||||
tracing::warn!("Bubblewrap requested but not available, falling back to application-layer");
|
||||
tracing::warn!(
|
||||
"Bubblewrap requested but not available, falling back to application-layer"
|
||||
);
|
||||
Arc::new(super::traits::NoopSandbox)
|
||||
}
|
||||
SandboxBackend::Docker => {
|
||||
|
|
@ -138,7 +144,7 @@ mod tests {
|
|||
fn auto_mode_detects_something() {
|
||||
let config = SecurityConfig {
|
||||
sandbox: SandboxConfig {
|
||||
enabled: None, // Auto-detect
|
||||
enabled: None, // Auto-detect
|
||||
backend: SandboxBackend::Auto,
|
||||
firejail_args: Vec::new(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,14 +56,21 @@ impl DockerSandbox {
|
|||
impl Sandbox for DockerSandbox {
|
||||
fn wrap_command(&self, cmd: &mut Command) -> std::io::Result<()> {
|
||||
let program = cmd.get_program().to_string_lossy().to_string();
|
||||
let args: Vec<String> = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect();
|
||||
let args: Vec<String> = cmd
|
||||
.get_args()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let mut docker_cmd = Command::new("docker");
|
||||
docker_cmd.args([
|
||||
"run", "--rm",
|
||||
"--memory", "512m",
|
||||
"--cpus", "1.0",
|
||||
"--network", "none",
|
||||
"run",
|
||||
"--rm",
|
||||
"--memory",
|
||||
"512m",
|
||||
"--cpus",
|
||||
"1.0",
|
||||
"--network",
|
||||
"none",
|
||||
]);
|
||||
docker_cmd.arg(&self.image);
|
||||
docker_cmd.arg(&program);
|
||||
|
|
|
|||
|
|
@ -41,20 +41,23 @@ impl Sandbox for FirejailSandbox {
|
|||
fn wrap_command(&self, cmd: &mut Command) -> std::io::Result<()> {
|
||||
// Prepend firejail to the command
|
||||
let program = cmd.get_program().to_string_lossy().to_string();
|
||||
let args: Vec<String> = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect();
|
||||
let args: Vec<String> = cmd
|
||||
.get_args()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
// Build firejail wrapper with security flags
|
||||
let mut firejail_cmd = Command::new("firejail");
|
||||
firejail_cmd.args([
|
||||
"--private=home", // New home directory
|
||||
"--private-dev", // Minimal /dev
|
||||
"--nosound", // No audio
|
||||
"--no3d", // No 3D acceleration
|
||||
"--novideo", // No video devices
|
||||
"--nowheel", // No input devices
|
||||
"--notv", // No TV devices
|
||||
"--noprofile", // Skip profile loading
|
||||
"--quiet", // Suppress warnings
|
||||
"--private=home", // New home directory
|
||||
"--private-dev", // Minimal /dev
|
||||
"--nosound", // No audio
|
||||
"--no3d", // No 3D acceleration
|
||||
"--novideo", // No video devices
|
||||
"--nowheel", // No input devices
|
||||
"--notv", // No TV devices
|
||||
"--noprofile", // Skip profile loading
|
||||
"--quiet", // Suppress warnings
|
||||
]);
|
||||
|
||||
// Add the original command
|
||||
|
|
@ -100,7 +103,10 @@ mod tests {
|
|||
let result = FirejailSandbox::new();
|
||||
match result {
|
||||
Ok(_) => println!("Firejail is installed"),
|
||||
Err(e) => assert!(e.kind() == std::io::ErrorKind::NotFound || e.kind() == std::io::ErrorKind::Unsupported),
|
||||
Err(e) => assert!(
|
||||
e.kind() == std::io::ErrorKind::NotFound
|
||||
|| e.kind() == std::io::ErrorKind::Unsupported
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ impl LandlockSandbox {
|
|||
/// Create a Landlock sandbox with a specific workspace directory
|
||||
pub fn with_workspace(workspace_dir: Option<std::path::PathBuf>) -> std::io::Result<Self> {
|
||||
// Test if Landlock is available by trying to create a minimal ruleset
|
||||
let test_ruleset = Ruleset::new()
|
||||
.set_access_fs(AccessFS::read_file | AccessFS::write_file);
|
||||
let test_ruleset = Ruleset::new().set_access_fs(AccessFS::read_file | AccessFS::write_file);
|
||||
|
||||
match test_ruleset.create() {
|
||||
Ok(_) => Ok(Self { workspace_dir }),
|
||||
|
|
@ -48,30 +47,35 @@ impl LandlockSandbox {
|
|||
|
||||
/// Apply Landlock restrictions to the current process
|
||||
fn apply_restrictions(&self) -> std::io::Result<()> {
|
||||
let mut ruleset = Ruleset::new()
|
||||
.set_access_fs(
|
||||
AccessFS::read_file
|
||||
| AccessFS::write_file
|
||||
| AccessFS::read_dir
|
||||
| AccessFS::remove_dir
|
||||
| AccessFS::remove_file
|
||||
| AccessFS::make_char
|
||||
| AccessFS::make_sock
|
||||
| AccessFS::make_fifo
|
||||
| AccessFS::make_block
|
||||
| AccessFS::make_reg
|
||||
| AccessFS::make_sym
|
||||
);
|
||||
let mut ruleset = Ruleset::new().set_access_fs(
|
||||
AccessFS::read_file
|
||||
| AccessFS::write_file
|
||||
| AccessFS::read_dir
|
||||
| AccessFS::remove_dir
|
||||
| AccessFS::remove_file
|
||||
| AccessFS::make_char
|
||||
| AccessFS::make_sock
|
||||
| AccessFS::make_fifo
|
||||
| AccessFS::make_block
|
||||
| AccessFS::make_reg
|
||||
| AccessFS::make_sym,
|
||||
);
|
||||
|
||||
// Allow workspace directory (read/write)
|
||||
if let Some(ref workspace) = self.workspace_dir {
|
||||
if workspace.exists() {
|
||||
ruleset = ruleset.add_path(workspace, AccessFS::read_file | AccessFS::write_file | AccessFS::read_dir)?;
|
||||
ruleset = ruleset.add_path(
|
||||
workspace,
|
||||
AccessFS::read_file | AccessFS::write_file | AccessFS::read_dir,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow /tmp for general operations
|
||||
ruleset = ruleset.add_path(Path::new("/tmp"), AccessFS::read_file | AccessFS::write_file)?;
|
||||
ruleset = ruleset.add_path(
|
||||
Path::new("/tmp"),
|
||||
AccessFS::read_file | AccessFS::write_file,
|
||||
)?;
|
||||
|
||||
// Allow /usr and /bin for executing commands
|
||||
ruleset = ruleset.add_path(Path::new("/usr"), AccessFS::read_file | AccessFS::read_dir)?;
|
||||
|
|
@ -193,7 +197,10 @@ mod tests {
|
|||
// Result depends on platform and feature flag
|
||||
match result {
|
||||
Ok(sandbox) => assert!(sandbox.is_available()),
|
||||
Err(_) => assert!(!cfg!(all(feature = "sandbox-landlock", target_os = "linux"))),
|
||||
Err(_) => assert!(!cfg!(all(
|
||||
feature = "sandbox-landlock",
|
||||
target_os = "linux"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub mod audit;
|
||||
pub mod detect;
|
||||
#[cfg(feature = "sandbox-bubblewrap")]
|
||||
pub mod bubblewrap;
|
||||
pub mod detect;
|
||||
pub mod docker;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod firejail;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ mod tests {
|
|||
let mut cmd = Command::new("echo");
|
||||
cmd.arg("test");
|
||||
let original_program = cmd.get_program().to_string_lossy().to_string();
|
||||
let original_args: Vec<String> = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect();
|
||||
let original_args: Vec<String> = cmd
|
||||
.get_args()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect();
|
||||
|
||||
let sandbox = NoopSandbox;
|
||||
assert!(sandbox.wrap_command(&mut cmd).is_ok());
|
||||
|
|
@ -69,7 +72,9 @@ mod tests {
|
|||
// Command should be unchanged
|
||||
assert_eq!(cmd.get_program().to_string_lossy(), original_program);
|
||||
assert_eq!(
|
||||
cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect::<Vec<_>>(),
|
||||
cmd.get_args()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
original_args
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue