mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 08:17:53 +00:00
* Use live owner tool scope for autonomous runs * Address autonomous tool scope review feedback * Normalize routine context paths again
211 lines
6.5 KiB
Rust
211 lines
6.5 KiB
Rust
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
|
|
use crate::extensions::ExtensionManager;
|
|
|
|
use super::ToolRegistry;
|
|
|
|
pub const AUTONOMOUS_TOOL_DENYLIST: &[&str] = &[
|
|
"routine_create",
|
|
"routine_update",
|
|
"routine_delete",
|
|
"routine_fire",
|
|
"event_emit",
|
|
"create_job",
|
|
"job_prompt",
|
|
"restart",
|
|
"tool_install",
|
|
"tool_auth",
|
|
"tool_activate",
|
|
"tool_remove",
|
|
"tool_upgrade",
|
|
"skill_install",
|
|
"skill_remove",
|
|
"secret_list",
|
|
"secret_delete",
|
|
];
|
|
|
|
pub fn is_autonomous_tool_denylisted(tool_name: &str) -> bool {
|
|
AUTONOMOUS_TOOL_DENYLIST.contains(&tool_name)
|
|
}
|
|
|
|
pub fn autonomous_unavailable_message(tool_name: &str, owner_id: &str) -> String {
|
|
if is_autonomous_tool_denylisted(tool_name) {
|
|
format!("Tool '{tool_name}' is not available in autonomous jobs or routines")
|
|
} else {
|
|
format!("Tool '{tool_name}' is not currently available for owner '{owner_id}'")
|
|
}
|
|
}
|
|
|
|
pub fn autonomous_unavailable_error(tool_name: &str, owner_id: &str) -> crate::error::ToolError {
|
|
crate::error::ToolError::AutonomousUnavailable {
|
|
name: tool_name.to_string(),
|
|
reason: autonomous_unavailable_message(tool_name, owner_id),
|
|
}
|
|
}
|
|
|
|
pub async fn autonomous_allowed_tool_names(
|
|
tools: &Arc<ToolRegistry>,
|
|
extension_manager: Option<&Arc<ExtensionManager>>,
|
|
owner_id: &str,
|
|
) -> HashSet<String> {
|
|
let mut allowed = tools.builtin_tool_names().await;
|
|
allowed.retain(|name| !is_autonomous_tool_denylisted(name));
|
|
|
|
if let Some(extension_manager) = extension_manager
|
|
&& extension_manager.owner_id() == owner_id
|
|
{
|
|
allowed.extend(
|
|
extension_manager
|
|
.active_tool_names()
|
|
.await
|
|
.into_iter()
|
|
.filter(|name| !is_autonomous_tool_denylisted(name)),
|
|
);
|
|
}
|
|
|
|
allowed
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use async_trait::async_trait;
|
|
use secrecy::SecretString;
|
|
|
|
use super::*;
|
|
use crate::context::JobContext;
|
|
use crate::extensions::ExtensionManager;
|
|
use crate::hooks::HookRegistry;
|
|
use crate::secrets::{InMemorySecretsStore, SecretsCrypto, SecretsStore};
|
|
use crate::tools::mcp::{McpProcessManager, McpSessionManager};
|
|
use crate::tools::{Tool, ToolError, ToolOutput};
|
|
|
|
struct FakeTool {
|
|
name: &'static str,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for FakeTool {
|
|
fn name(&self) -> &str {
|
|
self.name
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"test tool"
|
|
}
|
|
|
|
fn parameters_schema(&self) -> serde_json::Value {
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"properties": {},
|
|
})
|
|
}
|
|
|
|
async fn execute(
|
|
&self,
|
|
_params: serde_json::Value,
|
|
_ctx: &JobContext,
|
|
) -> Result<ToolOutput, ToolError> {
|
|
Ok(ToolOutput::text("ok", Duration::from_millis(1)))
|
|
}
|
|
}
|
|
|
|
async fn write_test_extension_wasm(tools_dir: &Path, name: &str) {
|
|
tokio::fs::create_dir_all(tools_dir)
|
|
.await
|
|
.expect("create test tools dir");
|
|
tokio::fs::write(tools_dir.join(format!("{name}.wasm")), b"\0asm")
|
|
.await
|
|
.expect("write wasm marker");
|
|
}
|
|
|
|
fn make_extension_manager(
|
|
tools: Arc<ToolRegistry>,
|
|
tools_dir: &Path,
|
|
owner_id: &str,
|
|
) -> Arc<ExtensionManager> {
|
|
let crypto = Arc::new(
|
|
SecretsCrypto::new(SecretString::from(
|
|
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
))
|
|
.expect("test crypto"),
|
|
);
|
|
let secrets: Arc<dyn SecretsStore + Send + Sync> =
|
|
Arc::new(InMemorySecretsStore::new(crypto));
|
|
|
|
Arc::new(ExtensionManager::new(
|
|
Arc::new(McpSessionManager::new()),
|
|
Arc::new(McpProcessManager::new()),
|
|
secrets,
|
|
tools,
|
|
Some(Arc::new(HookRegistry::default())),
|
|
None,
|
|
tools_dir.to_path_buf(),
|
|
tools_dir.join("channels"),
|
|
None,
|
|
owner_id.to_string(),
|
|
None,
|
|
Vec::new(),
|
|
))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn autonomous_scope_keeps_allowed_builtins_and_blocks_denylisted_builtins() {
|
|
let tools = Arc::new(ToolRegistry::new());
|
|
tools.register_sync(Arc::new(FakeTool { name: "echo" }));
|
|
tools.register_sync(Arc::new(FakeTool { name: "restart" }));
|
|
|
|
let allowed = autonomous_allowed_tool_names(&tools, None, "default").await;
|
|
|
|
assert!(allowed.contains("echo"));
|
|
assert!(!allowed.contains("restart"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn autonomous_scope_includes_active_extension_tools_for_matching_owner() {
|
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
|
let tools_dir = temp_dir.path().join("wasm-tools");
|
|
let tools = Arc::new(ToolRegistry::new());
|
|
tools
|
|
.register(Arc::new(FakeTool { name: "owner_gate" }))
|
|
.await;
|
|
write_test_extension_wasm(&tools_dir, "owner_gate").await;
|
|
let manager = make_extension_manager(tools.clone(), &tools_dir, "default");
|
|
|
|
let allowed = autonomous_allowed_tool_names(&tools, Some(&manager), "default").await;
|
|
|
|
assert!(allowed.contains("owner_gate"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn autonomous_scope_excludes_inactive_extension_tools() {
|
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
|
let tools_dir = temp_dir.path().join("wasm-tools");
|
|
let tools = Arc::new(ToolRegistry::new());
|
|
let manager = make_extension_manager(tools.clone(), &tools_dir, "default");
|
|
|
|
let allowed = autonomous_allowed_tool_names(&tools, Some(&manager), "default").await;
|
|
|
|
assert!(!allowed.contains("owner_gate"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn autonomous_scope_excludes_active_extension_tools_for_other_owner() {
|
|
let temp_dir = tempfile::tempdir().expect("tempdir");
|
|
let tools_dir = temp_dir.path().join("wasm-tools");
|
|
let tools = Arc::new(ToolRegistry::new());
|
|
tools
|
|
.register(Arc::new(FakeTool { name: "owner_gate" }))
|
|
.await;
|
|
write_test_extension_wasm(&tools_dir, "owner_gate").await;
|
|
let manager = make_extension_manager(tools.clone(), &tools_dir, "someone-else");
|
|
|
|
let allowed = autonomous_allowed_tool_names(&tools, Some(&manager), "default").await;
|
|
|
|
assert!(!allowed.contains("owner_gate"));
|
|
}
|
|
}
|