diff --git a/src/openhuman/agent/harness/session/builder.rs b/src/openhuman/agent/harness/session/builder.rs index 55da8a6662..724604fae8 100644 --- a/src/openhuman/agent/harness/session/builder.rs +++ b/src/openhuman/agent/harness/session/builder.rs @@ -768,6 +768,11 @@ impl Agent { &config.autonomy, &config.workspace_dir, )); + // Phase 1 of #1401: see comment in channels/runtime/startup.rs. + let audit = crate::openhuman::security::get_or_create_workspace_audit_logger( + crate::openhuman::config::AuditConfig::default(), + config.workspace_dir.clone(), + )?; let local_embedding = config.workload_local_model("embeddings"); let memory: Arc = Arc::from(memory::create_memory_with_local_ai( @@ -782,6 +787,7 @@ impl Agent { Arc::new(config.clone()), &security, runtime, + audit, memory.clone(), &config.browser, &config.http_request, diff --git a/src/openhuman/channels/runtime/startup.rs b/src/openhuman/channels/runtime/startup.rs index d61b8c7009..975dfc1737 100644 --- a/src/openhuman/channels/runtime/startup.rs +++ b/src/openhuman/channels/runtime/startup.rs @@ -180,6 +180,16 @@ pub async fn start_channels(config: Config) -> Result<()> { &config.autonomy, &config.workspace_dir, )); + // Phase 1 of #1401: audit logger is wired with defaults so emission paths + // are exercised at runtime. A follow-up promotes `SecurityConfig` (and + // therefore the `audit` knob) onto the runtime `Config` schema so users + // can override `enabled`, `log_path`, and `max_size_mb` via TOML. The + // logger is workspace-scoped and shared, so concurrent sessions append to + // one `audit.log` without racing on rotation. + let audit = crate::openhuman::security::get_or_create_workspace_audit_logger( + crate::openhuman::config::AuditConfig::default(), + config.workspace_dir.clone(), + )?; let model = config .default_model .clone() @@ -199,6 +209,7 @@ pub async fn start_channels(config: Config) -> Result<()> { Arc::new(config.clone()), &security, runtime, + audit, Arc::clone(&mem), &config.browser, &config.http_request, diff --git a/src/openhuman/runtime_node/ops.rs b/src/openhuman/runtime_node/ops.rs index 3eda43b50b..a0a757465a 100644 --- a/src/openhuman/runtime_node/ops.rs +++ b/src/openhuman/runtime_node/ops.rs @@ -39,6 +39,12 @@ fn build_runtime_tools(config: &Config) -> Result>, String> { &config.autonomy, &config.workspace_dir, )); + // Phase 1 of #1401: see comment in channels/runtime/startup.rs. + let audit = crate::openhuman::security::get_or_create_workspace_audit_logger( + crate::openhuman::config::AuditConfig::default(), + config.workspace_dir.clone(), + ) + .map_err(|e| e.to_string())?; let runtime: Arc = Arc::new(NativeRuntime::new()); let local_embedding = config.workload_local_model("embeddings"); trace!("[runtime_node::ops] build_runtime_tools: create_memory_with_local_ai"); @@ -63,6 +69,7 @@ fn build_runtime_tools(config: &Config) -> Result>, String> { Arc::new(config.clone()), &security, runtime, + audit, memory, &config.browser, &config.http_request, diff --git a/src/openhuman/security/audit.rs b/src/openhuman/security/audit.rs index 1dfcfaa593..e32da17614 100644 --- a/src/openhuman/security/audit.rs +++ b/src/openhuman/security/audit.rs @@ -5,9 +5,11 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Write; use std::path::PathBuf; +use std::sync::{Arc, OnceLock}; use uuid::Uuid; /// Audit event types @@ -148,6 +150,63 @@ pub struct AuditLogger { log_path: PathBuf, config: AuditConfig, buffer: Mutex>, + /// Serializes the rotate + append + fsync critical section so concurrent + /// callers sharing one logger cannot interleave partial JSON lines. + write_lock: Mutex<()>, +} + +/// Process-global cache of one [`AuditLogger`] per workspace directory. +/// +/// Multiple agent sessions in the same process share a workspace and therefore +/// the same `/audit.log`. Handing each session its own logger would +/// let concurrent appends race on rotation and interleave partial lines; this +/// registry guarantees a single `Arc` per workspace so all writes +/// serialize through one instance's `write_lock`. +static WORKSPACE_AUDIT_LOGGERS: OnceLock>>> = + OnceLock::new(); + +/// Return the shared [`AuditLogger`] for `openhuman_dir`, creating it on first +/// use. All callers targeting the same workspace receive the same `Arc`, so log +/// rotation and appends coordinate through one logger. The `config` of the first +/// caller for a given workspace wins; later callers reuse the cached logger. +pub fn get_or_create_workspace_audit_logger( + config: AuditConfig, + openhuman_dir: PathBuf, +) -> Result> { + // Normalize the key: `PathBuf` equality is lexical, so `/ws` vs `/ws/` vs a + // symlinked spelling would otherwise cache distinct loggers for one physical + // workspace and reopen the rotate/append race this registry prevents. + // + // A canonicalize failure falls back to the raw path rather than propagating: + // audit-logger creation must never block agent startup. `NotFound` (the + // workspace dir not created yet) is expected and logged at debug; other + // errors (permission, I/O) are unexpected and logged at warn so real + // filesystem problems stay observable. + let openhuman_dir = match std::fs::canonicalize(&openhuman_dir) { + Ok(path) => path, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + log::debug!( + "[openhuman:audit] workspace path not yet created; keying registry on raw path: {}", + openhuman_dir.display() + ); + openhuman_dir + } + Err(err) => { + log::warn!( + "[openhuman:audit] failed to canonicalize workspace path {} ({err}); keying registry on raw path", + openhuman_dir.display() + ); + openhuman_dir + } + }; + let registry = WORKSPACE_AUDIT_LOGGERS.get_or_init(|| Mutex::new(HashMap::new())); + let mut map = registry.lock(); + if let Some(existing) = map.get(&openhuman_dir) { + return Ok(Arc::clone(existing)); + } + let logger = Arc::new(AuditLogger::new(config, openhuman_dir.clone())?); + map.insert(openhuman_dir, Arc::clone(&logger)); + Ok(logger) } /// Structured command execution details for audit logging. @@ -163,6 +222,23 @@ pub struct CommandExecutionLog<'a> { } impl AuditLogger { + /// Build a disabled `Arc` for tests and contexts that need a + /// handle but should not write to disk. The `enabled = false` flag + /// short-circuits `log()` before any filesystem I/O, so the sentinel + /// `log_path` is never touched. + pub fn disabled() -> Arc { + Arc::new(Self { + log_path: PathBuf::new(), + config: AuditConfig { + enabled: false, + log_path: String::new(), + max_size_mb: 0, + }, + buffer: Mutex::new(Vec::new()), + write_lock: Mutex::new(()), + }) + } + /// Create a new audit logger pub fn new(config: AuditConfig, openhuman_dir: PathBuf) -> Result { let log_path = openhuman_dir.join(&config.log_path); @@ -175,6 +251,7 @@ impl AuditLogger { log_path, config, buffer: Mutex::new(Vec::new()), + write_lock: Mutex::new(()), }) } @@ -190,11 +267,14 @@ impl AuditLogger { event.event_id ); - // Check log size and rotate if needed - self.rotate_if_needed()?; - - // Serialize and write + // Serialize the event outside the write lock — formatting is pure. let line = serde_json::to_string(event)?; + + // Hold `write_lock` across rotate + append + fsync so concurrent + // callers sharing this logger cannot interleave partial lines or + // race on rotation renames. + let _guard = self.write_lock.lock(); + self.rotate_if_needed()?; let mut file = OpenOptions::new() .create(true) .append(true) @@ -331,6 +411,28 @@ mod tests { assert!(parsed.result.is_some()); } + #[test] + fn audit_logger_disabled_helper_is_noop() -> Result<()> { + let logger = AuditLogger::disabled(); + let event = AuditEvent::new(AuditEventType::CommandExecution); + logger.log(&event)?; + assert!(!logger.config.enabled); + Ok(()) + } + + #[test] + fn workspace_audit_logger_is_shared_per_workspace() -> Result<()> { + let tmp = TempDir::new()?; + let cfg = AuditConfig::default(); + let first = get_or_create_workspace_audit_logger(cfg.clone(), tmp.path().to_path_buf())?; + let second = get_or_create_workspace_audit_logger(cfg, tmp.path().to_path_buf())?; + assert!( + Arc::ptr_eq(&first, &second), + "same workspace must yield the same shared logger instance" + ); + Ok(()) + } + #[test] fn audit_logger_disabled_does_not_create_file() -> Result<()> { let tmp = TempDir::new()?; diff --git a/src/openhuman/security/mod.rs b/src/openhuman/security/mod.rs index 969e2047dd..52c80bf131 100644 --- a/src/openhuman/security/mod.rs +++ b/src/openhuman/security/mod.rs @@ -14,7 +14,10 @@ pub mod secrets; pub mod traits; #[allow(unused_imports)] -pub use audit::{AuditEvent, AuditEventType, AuditLogger}; +pub use audit::{ + get_or_create_workspace_audit_logger, AuditEvent, AuditEventType, AuditLogger, + CommandExecutionLog, +}; pub use core::*; #[allow(unused_imports)] pub use detect::create_sandbox; diff --git a/src/openhuman/tools/impl/system/shell.rs b/src/openhuman/tools/impl/system/shell.rs index d1cf0312e2..d9533c8d6f 100644 --- a/src/openhuman/tools/impl/system/shell.rs +++ b/src/openhuman/tools/impl/system/shell.rs @@ -1,11 +1,11 @@ use crate::openhuman::agent::host_runtime::RuntimeAdapter; use crate::openhuman::javascript::NodeBootstrap; -use crate::openhuman::security::SecurityPolicy; +use crate::openhuman::security::{AuditLogger, CommandExecutionLog, SecurityPolicy}; use crate::openhuman::tools::traits::{Tool, ToolResult}; use async_trait::async_trait; use serde_json::json; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; /// Maximum shell command execution time before kill. const SHELL_TIMEOUT_SECS: u64 = 60; @@ -21,6 +21,7 @@ const SAFE_ENV_VARS: &[&str] = &[ pub struct ShellTool { security: Arc, runtime: Arc, + audit: Arc, /// Optional managed Node.js bootstrap. When provided **and** a prior /// `NodeBootstrap::resolve()` has already succeeded, every shell invocation /// transparently prepends the managed `bin/` dir to `PATH` — so skills @@ -31,10 +32,15 @@ pub struct ShellTool { } impl ShellTool { - pub fn new(security: Arc, runtime: Arc) -> Self { + pub fn new( + security: Arc, + runtime: Arc, + audit: Arc, + ) -> Self { Self { security, runtime, + audit, node_bootstrap: None, } } @@ -45,14 +51,45 @@ impl ShellTool { pub fn with_node_bootstrap( security: Arc, runtime: Arc, + audit: Arc, bootstrap: Arc, ) -> Self { Self { security, runtime, + audit, node_bootstrap: Some(bootstrap), } } + + /// Emit a single `CommandExecution` audit event. A write failure is logged + /// as a structured warning but not propagated — audit must never block or + /// fail a tool call, yet a silently broken audit trail must not go + /// unnoticed. + fn emit_audit( + &self, + command: &str, + approved: bool, + allowed: bool, + success: bool, + duration_ms: u64, + ) { + if let Err(error) = self.audit.log_command_event(CommandExecutionLog { + channel: "tool:shell", + command, + risk_level: "unknown", + approved, + allowed, + success, + duration_ms, + }) { + tracing::warn!( + error = %error, + channel = "tool:shell", + "[shell] failed to persist command execution audit event" + ); + } + } } #[async_trait] @@ -102,23 +139,35 @@ impl Tool for ShellTool { .and_then(|v| v.as_bool()) .unwrap_or(false); + let start = Instant::now(); + let (allowed, result) = self.run_with_security(command, approved).await; + let duration_ms = u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX); + self.emit_audit(command, approved, allowed, !result.is_error, duration_ms); + Ok(result) + } +} + +impl ShellTool { + /// Run the command through the security policy and runtime. Returns + /// `(allowed, result)` where `allowed=false` means the policy or rate + /// limiter blocked execution before the command was launched. + async fn run_with_security(&self, command: &str, approved: bool) -> (bool, ToolResult) { if self.security.is_rate_limited() { - return Ok(ToolResult::error( - "Rate limit exceeded: too many actions in the last hour", - )); + return ( + false, + ToolResult::error("Rate limit exceeded: too many actions in the last hour"), + ); } - match self.security.validate_command_execution(command, approved) { - Ok(_) => {} - Err(reason) => { - return Ok(ToolResult::error(reason)); - } + if let Err(reason) = self.security.validate_command_execution(command, approved) { + return (false, ToolResult::error(reason)); } if !self.security.record_action() { - return Ok(ToolResult::error( - "Rate limit exceeded: action budget exhausted", - )); + return ( + false, + ToolResult::error("Rate limit exceeded: action budget exhausted"), + ); } // Execute with timeout to prevent hanging commands. @@ -130,9 +179,10 @@ impl Tool for ShellTool { { Ok(cmd) => cmd, Err(e) => { - return Ok(ToolResult::error(format!( - "Failed to build runtime command: {e}" - ))); + return ( + true, + ToolResult::error(format!("Failed to build runtime command: {e}")), + ); } }; cmd.env_clear(); @@ -168,7 +218,7 @@ impl Tool for ShellTool { let result = tokio::time::timeout(Duration::from_secs(SHELL_TIMEOUT_SECS), cmd.output()).await; - match result { + let tool_result = match result { Ok(Ok(output)) => { let mut stdout = String::from_utf8_lossy(&output.stdout).to_string(); let mut stderr = String::from_utf8_lossy(&output.stderr).to_string(); @@ -191,21 +241,22 @@ impl Tool for ShellTool { if output.status.success() { if stderr.is_empty() { - Ok(ToolResult::success(stdout)) + ToolResult::success(stdout) } else { // Successful exit but stderr present — attach stderr as output suffix - Ok(ToolResult::success(format!("{stdout}\n[stderr]\n{stderr}"))) + ToolResult::success(format!("{stdout}\n[stderr]\n{stderr}")) } } else { let err_msg = if stderr.is_empty() { stdout } else { stderr }; - Ok(ToolResult::error(err_msg)) + ToolResult::error(err_msg) } } - Ok(Err(e)) => Ok(ToolResult::error(format!("Failed to execute command: {e}"))), - Err(_) => Ok(ToolResult::error(format!( + Ok(Err(e)) => ToolResult::error(format!("Failed to execute command: {e}")), + Err(_) => ToolResult::error(format!( "Command timed out after {SHELL_TIMEOUT_SECS}s and was killed" - ))), - } + )), + }; + (true, tool_result) } } @@ -227,21 +278,99 @@ mod tests { Arc::new(NativeRuntime::new()) } + fn test_audit() -> Arc { + AuditLogger::disabled() + } + + fn audit_with_tempdir() -> (Arc, tempfile::TempDir) { + use crate::openhuman::config::AuditConfig; + let tmp = tempfile::tempdir().expect("create tempdir"); + let logger = AuditLogger::new( + AuditConfig { + enabled: true, + log_path: "audit.log".into(), + max_size_mb: 10, + }, + tmp.path().to_path_buf(), + ) + .expect("create audit logger"); + (Arc::new(logger), tmp) + } + + #[cfg(not(windows))] + #[tokio::test] + async fn shell_emits_audit_line_on_success() { + use crate::openhuman::security::AuditEvent; + let (audit, tmp) = audit_with_tempdir(); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + audit, + ); + let _ = tool + .execute(json!({"command": "echo hello"})) + .await + .unwrap(); + let log = std::fs::read_to_string(tmp.path().join("audit.log")) + .expect("audit log file should exist"); + assert!(!log.is_empty(), "audit log should not be empty"); + let parsed: AuditEvent = serde_json::from_str(log.trim()).expect("audit event JSON parses"); + let action = parsed.action.expect("action present"); + assert_eq!(action.command, Some("echo hello".to_string())); + assert!(action.allowed, "allowed command should set allowed=true"); + let result = parsed.result.expect("result present"); + assert!(result.success, "echo hello should succeed"); + let actor = parsed.actor.expect("actor present"); + assert_eq!(actor.channel, "tool:shell"); + } + + #[tokio::test] + async fn shell_emits_audit_line_on_denial() { + use crate::openhuman::security::AuditEvent; + let (audit, tmp) = audit_with_tempdir(); + let tool = ShellTool::new( + test_security(AutonomyLevel::ReadOnly), + test_runtime(), + audit, + ); + let _ = tool.execute(json!({"command": "ls"})).await.unwrap(); + let log = std::fs::read_to_string(tmp.path().join("audit.log")) + .expect("audit log file should exist"); + let parsed: AuditEvent = serde_json::from_str(log.trim()).expect("audit event JSON parses"); + let action = parsed.action.expect("action present"); + assert!( + !action.allowed, + "denied command should set allowed=false on the audit event" + ); + } + #[test] fn shell_tool_name() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); assert_eq!(tool.name(), "shell"); } #[test] fn shell_tool_description() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); assert!(!tool.description().is_empty()); } #[test] fn shell_tool_schema_has_command() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let schema = tool.parameters_schema(); assert!(schema["properties"]["command"].is_object()); assert!(schema["required"] @@ -254,7 +383,11 @@ mod tests { #[cfg(not(windows))] #[tokio::test] async fn shell_executes_allowed_command() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let result = tool .execute(json!({"command": "echo hello"})) .await @@ -266,7 +399,11 @@ mod tests { #[tokio::test] async fn shell_blocks_disallowed_command() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let result = tool.execute(json!({"command": "rm -rf /"})).await.unwrap(); assert!(result.is_error); let error = result.output(); @@ -275,7 +412,11 @@ mod tests { #[tokio::test] async fn shell_blocks_readonly() { - let tool = ShellTool::new(test_security(AutonomyLevel::ReadOnly), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::ReadOnly), + test_runtime(), + test_audit(), + ); let result = tool.execute(json!({"command": "ls"})).await.unwrap(); assert!(result.is_error); assert!(&result.output().contains("not allowed")); @@ -283,7 +424,11 @@ mod tests { #[tokio::test] async fn shell_missing_command_param() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let result = tool.execute(json!({})).await; assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("command")); @@ -291,14 +436,22 @@ mod tests { #[tokio::test] async fn shell_wrong_type_param() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let result = tool.execute(json!({"command": 123})).await; assert!(result.is_err()); } #[tokio::test] async fn shell_captures_exit_code() { - let tool = ShellTool::new(test_security(AutonomyLevel::Supervised), test_runtime()); + let tool = ShellTool::new( + test_security(AutonomyLevel::Supervised), + test_runtime(), + test_audit(), + ); let result = tool .execute(json!({"command": "ls /nonexistent_dir_xyz"})) .await @@ -344,7 +497,7 @@ mod tests { async fn shell_does_not_leak_api_key() { let _g1 = EnvGuard::set("API_KEY", "sk-test-secret-12345"); - let tool = ShellTool::new(test_security_with_env_cmd(), test_runtime()); + let tool = ShellTool::new(test_security_with_env_cmd(), test_runtime(), test_audit()); let result = tool .execute(json!({"command": "echo $API_KEY"})) .await @@ -359,7 +512,7 @@ mod tests { #[cfg(not(windows))] #[tokio::test] async fn shell_preserves_path_and_home() { - let tool = ShellTool::new(test_security_with_env_cmd(), test_runtime()); + let tool = ShellTool::new(test_security_with_env_cmd(), test_runtime(), test_audit()); let result = tool .execute(json!({"command": "echo $HOME"})) @@ -392,7 +545,7 @@ mod tests { ..SecurityPolicy::default() }); - let tool = ShellTool::new(security.clone(), test_runtime()); + let tool = ShellTool::new(security.clone(), test_runtime(), test_audit()); let command = if cfg!(windows) { "mkdir openhuman_shell_approval_test" } else { @@ -471,7 +624,7 @@ mod tests { workspace_dir: std::env::temp_dir(), ..SecurityPolicy::default() }); - let tool = ShellTool::new(security, test_runtime()); + let tool = ShellTool::new(security, test_runtime(), test_audit()); let result = tool.execute(json!({"command": "echo test"})).await.unwrap(); assert!(result.is_error); assert!(result.output().contains("Rate limit")); diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index 9aafb26627..c5ad0889d7 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -4,7 +4,7 @@ use crate::openhuman::agent::host_runtime::{NativeRuntime, RuntimeAdapter}; use crate::openhuman::config::{Config, DelegateAgentConfig}; use crate::openhuman::javascript::NodeBootstrap; use crate::openhuman::memory::Memory; -use crate::openhuman::security::SecurityPolicy; +use crate::openhuman::security::{AuditLogger, SecurityPolicy}; use std::collections::HashMap; use std::sync::Arc; @@ -14,12 +14,18 @@ pub fn default_tools(security: Arc) -> Vec> { } /// Create the default tool registry with explicit runtime adapter. +/// +/// Convenience entry point used by tests and the lightweight CLI surface. +/// Production assembly sites use [`all_tools_with_runtime`] and pass a real +/// [`AuditLogger`]; this wrapper substitutes [`AuditLogger::disabled`] so +/// existing test callers do not need to plumb one through. pub fn default_tools_with_runtime( security: Arc, runtime: Arc, ) -> Vec> { + let audit = AuditLogger::disabled(); vec![ - Box::new(ShellTool::new(security.clone(), runtime)), + Box::new(ShellTool::new(security.clone(), runtime, audit)), Box::new(FileReadTool::new(security.clone())), Box::new(FileWriteTool::new(security)), ] @@ -30,6 +36,7 @@ pub fn default_tools_with_runtime( pub fn all_tools( config: Arc, security: &Arc, + audit: Arc, memory: Arc, browser_config: &crate::openhuman::config::BrowserConfig, http_config: &crate::openhuman::config::HttpRequestConfig, @@ -41,6 +48,7 @@ pub fn all_tools( config, security, Arc::new(NativeRuntime::new()), + audit, memory, browser_config, http_config, @@ -56,6 +64,7 @@ pub fn all_tools_with_runtime( config: Arc, security: &Arc, runtime: Arc, + audit: Arc, memory: Arc, browser_config: &crate::openhuman::config::BrowserConfig, http_config: &crate::openhuman::config::HttpRequestConfig, @@ -89,10 +98,15 @@ pub fn all_tools_with_runtime( Box::new(ShellTool::with_node_bootstrap( security.clone(), Arc::clone(&runtime), + Arc::clone(&audit), Arc::clone(bootstrap), )) } else { - Box::new(ShellTool::new(security.clone(), Arc::clone(&runtime))) + Box::new(ShellTool::new( + security.clone(), + Arc::clone(&runtime), + Arc::clone(&audit), + )) }; let mut tools: Vec> = vec![ diff --git a/src/openhuman/tools/ops_tests.rs b/src/openhuman/tools/ops_tests.rs index 1b9e3672be..d3171aff65 100644 --- a/src/openhuman/tools/ops_tests.rs +++ b/src/openhuman/tools/ops_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::openhuman::config::{BrowserConfig, Config, MemoryConfig}; use crate::openhuman::credentials::{AuthService, APP_SESSION_PROVIDER, DEFAULT_AUTH_PROFILE_NAME}; +use crate::openhuman::security::AuditLogger; use tempfile::TempDir; #[path = "../integrations/test_support.rs"] @@ -66,6 +67,7 @@ fn integration_tools_for_config(tmp: &TempDir, cfg: &Config) -> Vec