feat(proxy): add scoped proxy configuration and docs runbooks
- add scope-aware proxy schema and runtime wiring for providers/channels/tools - add agent callable proxy_config tool for fast proxy setup - standardize docs system with index, template, and playbooks
This commit is contained in:
parent
13ee9e6398
commit
ce104bed45
36 changed files with 2025 additions and 323 deletions
|
|
@ -736,7 +736,7 @@ impl BrowserTool {
|
|||
}
|
||||
});
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let client = crate::config::build_runtime_proxy_client("tool.browser");
|
||||
let mut request = client
|
||||
.post(endpoint)
|
||||
.timeout(Duration::from_millis(self.computer_use.timeout_ms))
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ pub struct ComposioTool {
|
|||
api_key: String,
|
||||
default_entity_id: String,
|
||||
security: Arc<SecurityPolicy>,
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl ComposioTool {
|
||||
|
|
@ -37,14 +36,13 @@ impl ComposioTool {
|
|||
api_key: api_key.to_string(),
|
||||
default_entity_id: normalize_entity_id(default_entity_id.unwrap_or("default")),
|
||||
security,
|
||||
client: Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn client(&self) -> Client {
|
||||
crate::config::build_runtime_proxy_client_with_timeouts("tool.composio", 60, 10)
|
||||
}
|
||||
|
||||
/// List available Composio apps/actions for the authenticated user.
|
||||
///
|
||||
/// Uses v3 endpoint first and falls back to v2 for compatibility.
|
||||
|
|
@ -68,7 +66,7 @@ impl ComposioTool {
|
|||
|
||||
async fn list_actions_v3(&self, app_name: Option<&str>) -> anyhow::Result<Vec<ComposioAction>> {
|
||||
let url = format!("{COMPOSIO_API_BASE_V3}/tools");
|
||||
let mut req = self.client.get(&url).header("x-api-key", &self.api_key);
|
||||
let mut req = self.client().get(&url).header("x-api-key", &self.api_key);
|
||||
|
||||
req = req.query(&[("limit", "200")]);
|
||||
if let Some(app) = app_name.map(str::trim).filter(|app| !app.is_empty()) {
|
||||
|
|
@ -95,7 +93,7 @@ impl ComposioTool {
|
|||
}
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.get(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.send()
|
||||
|
|
@ -180,7 +178,7 @@ impl ComposioTool {
|
|||
);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.post(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.json(&body)
|
||||
|
|
@ -216,7 +214,7 @@ impl ComposioTool {
|
|||
}
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.post(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.json(&body)
|
||||
|
|
@ -288,7 +286,7 @@ impl ComposioTool {
|
|||
});
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.post(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.json(&body)
|
||||
|
|
@ -321,7 +319,7 @@ impl ComposioTool {
|
|||
});
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.post(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.json(&body)
|
||||
|
|
@ -345,7 +343,7 @@ impl ComposioTool {
|
|||
let url = format!("{COMPOSIO_API_BASE_V3}/auth_configs");
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.client()
|
||||
.get(&url)
|
||||
.header("x-api-key", &self.api_key)
|
||||
.query(&[
|
||||
|
|
|
|||
|
|
@ -114,10 +114,12 @@ impl HttpRequestTool {
|
|||
headers: Vec<(String, String)>,
|
||||
body: Option<&str>,
|
||||
) -> anyhow::Result<reqwest::Response> {
|
||||
let client = reqwest::Client::builder()
|
||||
let builder = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(self.timeout_secs))
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.build()?;
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.redirect(reqwest::redirect::Policy::none());
|
||||
let builder = crate::config::apply_runtime_proxy_to_builder(builder, "tool.http_request");
|
||||
let client = builder.build()?;
|
||||
|
||||
let mut request = client.request(method, url);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod image_info;
|
|||
pub mod memory_forget;
|
||||
pub mod memory_recall;
|
||||
pub mod memory_store;
|
||||
pub mod proxy_config;
|
||||
pub mod pushover;
|
||||
pub mod schedule;
|
||||
pub mod schema;
|
||||
|
|
@ -48,6 +49,7 @@ pub use image_info::ImageInfoTool;
|
|||
pub use memory_forget::MemoryForgetTool;
|
||||
pub use memory_recall::MemoryRecallTool;
|
||||
pub use memory_store::MemoryStoreTool;
|
||||
pub use proxy_config::ProxyConfigTool;
|
||||
pub use pushover::PushoverTool;
|
||||
pub use schedule::ScheduleTool;
|
||||
#[allow(unused_imports)]
|
||||
|
|
@ -144,6 +146,7 @@ pub fn all_tools_with_runtime(
|
|||
Box::new(MemoryRecallTool::new(memory.clone())),
|
||||
Box::new(MemoryForgetTool::new(memory, security.clone())),
|
||||
Box::new(ScheduleTool::new(security.clone(), root_config.clone())),
|
||||
Box::new(ProxyConfigTool::new(config.clone(), security.clone())),
|
||||
Box::new(GitOperationsTool::new(
|
||||
security.clone(),
|
||||
workspace_dir.to_path_buf(),
|
||||
|
|
@ -292,6 +295,7 @@ mod tests {
|
|||
assert!(!names.contains(&"browser_open"));
|
||||
assert!(names.contains(&"schedule"));
|
||||
assert!(names.contains(&"pushover"));
|
||||
assert!(names.contains(&"proxy_config"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -330,6 +334,7 @@ mod tests {
|
|||
let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
|
||||
assert!(names.contains(&"browser_open"));
|
||||
assert!(names.contains(&"pushover"));
|
||||
assert!(names.contains(&"proxy_config"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
492
src/tools/proxy_config.rs
Normal file
492
src/tools/proxy_config.rs
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
use super::traits::{Tool, ToolResult};
|
||||
use crate::config::{
|
||||
runtime_proxy_config, set_runtime_proxy_config, Config, ProxyConfig, ProxyScope,
|
||||
};
|
||||
use crate::security::SecurityPolicy;
|
||||
use async_trait::async_trait;
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ProxyConfigTool {
|
||||
config: Arc<Config>,
|
||||
security: Arc<SecurityPolicy>,
|
||||
}
|
||||
|
||||
impl ProxyConfigTool {
|
||||
pub fn new(config: Arc<Config>, security: Arc<SecurityPolicy>) -> Self {
|
||||
Self { config, security }
|
||||
}
|
||||
|
||||
fn load_config_without_env(&self) -> anyhow::Result<Config> {
|
||||
let contents = fs::read_to_string(&self.config.config_path).map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to read config file {}: {error}",
|
||||
self.config.config_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut parsed: Config = toml::from_str(&contents).map_err(|error| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to parse config file {}: {error}",
|
||||
self.config.config_path.display()
|
||||
)
|
||||
})?;
|
||||
parsed.config_path = self.config.config_path.clone();
|
||||
parsed.workspace_dir = self.config.workspace_dir.clone();
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
fn require_write_access(&self) -> Option<ToolResult> {
|
||||
if !self.security.can_act() {
|
||||
return Some(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Action blocked: autonomy is read-only".into()),
|
||||
});
|
||||
}
|
||||
|
||||
if !self.security.record_action() {
|
||||
return Some(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some("Action blocked: rate limit exceeded".into()),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_scope(raw: &str) -> Option<ProxyScope> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"environment" | "env" => Some(ProxyScope::Environment),
|
||||
"zeroclaw" | "internal" | "core" => Some(ProxyScope::Zeroclaw),
|
||||
"services" | "service" => Some(ProxyScope::Services),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_string_list(raw: &Value, field: &str) -> anyhow::Result<Vec<String>> {
|
||||
if let Some(raw_string) = raw.as_str() {
|
||||
return Ok(raw_string
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|entry| !entry.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect());
|
||||
}
|
||||
|
||||
if let Some(array) = raw.as_array() {
|
||||
let mut out = Vec::new();
|
||||
for item in array {
|
||||
let value = item
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("'{field}' array must only contain strings"))?;
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
out.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
anyhow::bail!("'{field}' must be a string or string[]")
|
||||
}
|
||||
|
||||
fn parse_optional_string_update(
|
||||
args: &Value,
|
||||
field: &str,
|
||||
) -> anyhow::Result<Option<Option<String>>> {
|
||||
let Some(raw) = args.get(field) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if raw.is_null() {
|
||||
return Ok(Some(None));
|
||||
}
|
||||
|
||||
let value = raw
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("'{field}' must be a string or null"))?
|
||||
.trim()
|
||||
.to_string();
|
||||
Ok(Some((!value.is_empty()).then_some(value)))
|
||||
}
|
||||
|
||||
fn env_snapshot() -> Value {
|
||||
json!({
|
||||
"HTTP_PROXY": std::env::var("HTTP_PROXY").ok(),
|
||||
"HTTPS_PROXY": std::env::var("HTTPS_PROXY").ok(),
|
||||
"ALL_PROXY": std::env::var("ALL_PROXY").ok(),
|
||||
"NO_PROXY": std::env::var("NO_PROXY").ok(),
|
||||
})
|
||||
}
|
||||
|
||||
fn proxy_json(proxy: &ProxyConfig) -> Value {
|
||||
json!({
|
||||
"enabled": proxy.enabled,
|
||||
"scope": proxy.scope,
|
||||
"http_proxy": proxy.http_proxy,
|
||||
"https_proxy": proxy.https_proxy,
|
||||
"all_proxy": proxy.all_proxy,
|
||||
"no_proxy": proxy.normalized_no_proxy(),
|
||||
"services": proxy.normalized_services(),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_get(&self) -> anyhow::Result<ToolResult> {
|
||||
let file_proxy = self.load_config_without_env()?.proxy;
|
||||
let runtime_proxy = runtime_proxy_config();
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"proxy": Self::proxy_json(&file_proxy),
|
||||
"runtime_proxy": Self::proxy_json(&runtime_proxy),
|
||||
"environment": Self::env_snapshot(),
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_list_services(&self) -> anyhow::Result<ToolResult> {
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"supported_service_keys": ProxyConfig::supported_service_keys(),
|
||||
"supported_selectors": ProxyConfig::supported_service_selectors(),
|
||||
"usage_example": {
|
||||
"action": "set",
|
||||
"scope": "services",
|
||||
"services": ["provider.openai", "tool.http_request", "channel.telegram"]
|
||||
}
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_set(&self, args: &Value) -> anyhow::Result<ToolResult> {
|
||||
let mut cfg = self.load_config_without_env()?;
|
||||
let previous_scope = cfg.proxy.scope;
|
||||
let mut proxy = cfg.proxy.clone();
|
||||
let mut touched_proxy_url = false;
|
||||
|
||||
if let Some(enabled) = args.get("enabled") {
|
||||
proxy.enabled = enabled
|
||||
.as_bool()
|
||||
.ok_or_else(|| anyhow::anyhow!("'enabled' must be a boolean"))?;
|
||||
}
|
||||
|
||||
if let Some(scope_raw) = args.get("scope") {
|
||||
let scope = scope_raw
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("'scope' must be a string"))?;
|
||||
proxy.scope = Self::parse_scope(scope).ok_or_else(|| {
|
||||
anyhow::anyhow!("Invalid scope '{scope}'. Use environment|zeroclaw|services")
|
||||
})?;
|
||||
}
|
||||
|
||||
if let Some(update) = Self::parse_optional_string_update(args, "http_proxy")? {
|
||||
proxy.http_proxy = update;
|
||||
touched_proxy_url = true;
|
||||
}
|
||||
|
||||
if let Some(update) = Self::parse_optional_string_update(args, "https_proxy")? {
|
||||
proxy.https_proxy = update;
|
||||
touched_proxy_url = true;
|
||||
}
|
||||
|
||||
if let Some(update) = Self::parse_optional_string_update(args, "all_proxy")? {
|
||||
proxy.all_proxy = update;
|
||||
touched_proxy_url = true;
|
||||
}
|
||||
|
||||
if let Some(no_proxy_raw) = args.get("no_proxy") {
|
||||
proxy.no_proxy = Self::parse_string_list(no_proxy_raw, "no_proxy")?;
|
||||
}
|
||||
|
||||
if let Some(services_raw) = args.get("services") {
|
||||
proxy.services = Self::parse_string_list(services_raw, "services")?;
|
||||
}
|
||||
|
||||
if args.get("enabled").is_none() && touched_proxy_url {
|
||||
proxy.enabled = true;
|
||||
}
|
||||
|
||||
proxy.no_proxy = proxy.normalized_no_proxy();
|
||||
proxy.services = proxy.normalized_services();
|
||||
proxy.validate()?;
|
||||
|
||||
cfg.proxy = proxy.clone();
|
||||
cfg.save()?;
|
||||
set_runtime_proxy_config(proxy.clone());
|
||||
|
||||
if proxy.enabled && proxy.scope == ProxyScope::Environment {
|
||||
proxy.apply_to_process_env();
|
||||
} else if previous_scope == ProxyScope::Environment {
|
||||
ProxyConfig::clear_process_env();
|
||||
}
|
||||
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"message": "Proxy configuration updated",
|
||||
"proxy": Self::proxy_json(&proxy),
|
||||
"environment": Self::env_snapshot(),
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_disable(&self, args: &Value) -> anyhow::Result<ToolResult> {
|
||||
let mut cfg = self.load_config_without_env()?;
|
||||
let clear_env_default = cfg.proxy.scope == ProxyScope::Environment;
|
||||
cfg.proxy.enabled = false;
|
||||
cfg.save()?;
|
||||
|
||||
set_runtime_proxy_config(cfg.proxy.clone());
|
||||
|
||||
let clear_env = args
|
||||
.get("clear_env")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(clear_env_default);
|
||||
if clear_env {
|
||||
ProxyConfig::clear_process_env();
|
||||
}
|
||||
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"message": "Proxy disabled",
|
||||
"proxy": Self::proxy_json(&cfg.proxy),
|
||||
"environment": Self::env_snapshot(),
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_apply_env(&self) -> anyhow::Result<ToolResult> {
|
||||
let cfg = self.load_config_without_env()?;
|
||||
let proxy = cfg.proxy;
|
||||
proxy.validate()?;
|
||||
|
||||
if !proxy.enabled {
|
||||
anyhow::bail!("Proxy is disabled. Use action 'set' with enabled=true first");
|
||||
}
|
||||
|
||||
if proxy.scope != ProxyScope::Environment {
|
||||
anyhow::bail!(
|
||||
"apply_env only works when proxy.scope is 'environment' (current: {:?})",
|
||||
proxy.scope
|
||||
);
|
||||
}
|
||||
|
||||
proxy.apply_to_process_env();
|
||||
set_runtime_proxy_config(proxy.clone());
|
||||
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"message": "Proxy environment variables applied",
|
||||
"proxy": Self::proxy_json(&proxy),
|
||||
"environment": Self::env_snapshot(),
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_clear_env(&self) -> anyhow::Result<ToolResult> {
|
||||
ProxyConfig::clear_process_env();
|
||||
Ok(ToolResult {
|
||||
success: true,
|
||||
output: serde_json::to_string_pretty(&json!({
|
||||
"message": "Proxy environment variables cleared",
|
||||
"environment": Self::env_snapshot(),
|
||||
}))?,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for ProxyConfigTool {
|
||||
fn name(&self) -> &str {
|
||||
"proxy_config"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Manage ZeroClaw proxy settings (scope: environment | zeroclaw | services), including runtime and process env application"
|
||||
}
|
||||
|
||||
fn parameters_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["get", "set", "disable", "list_services", "apply_env", "clear_env"],
|
||||
"default": "get"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable or disable proxy"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Proxy scope: environment | zeroclaw | services"
|
||||
},
|
||||
"http_proxy": {
|
||||
"type": ["string", "null"],
|
||||
"description": "HTTP proxy URL"
|
||||
},
|
||||
"https_proxy": {
|
||||
"type": ["string", "null"],
|
||||
"description": "HTTPS proxy URL"
|
||||
},
|
||||
"all_proxy": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Fallback proxy URL for all protocols"
|
||||
},
|
||||
"no_proxy": {
|
||||
"description": "Comma-separated string or array of NO_PROXY entries",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"services": {
|
||||
"description": "Comma-separated string or array of service selectors used when scope=services",
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "array", "items": {"type": "string"}}
|
||||
]
|
||||
},
|
||||
"clear_env": {
|
||||
"type": "boolean",
|
||||
"description": "When action=disable, clear process proxy environment variables"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
|
||||
let action = args
|
||||
.get("action")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("get")
|
||||
.to_ascii_lowercase();
|
||||
|
||||
let result = match action.as_str() {
|
||||
"get" => self.handle_get(),
|
||||
"list_services" => self.handle_list_services(),
|
||||
"set" | "disable" | "apply_env" | "clear_env" => {
|
||||
if let Some(blocked) = self.require_write_access() {
|
||||
return Ok(blocked);
|
||||
}
|
||||
|
||||
match action.as_str() {
|
||||
"set" => self.handle_set(&args),
|
||||
"disable" => self.handle_disable(&args),
|
||||
"apply_env" => self.handle_apply_env(),
|
||||
"clear_env" => self.handle_clear_env(),
|
||||
_ => unreachable!("handled above"),
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!(
|
||||
"Unknown action '{action}'. Valid: get, set, disable, list_services, apply_env, clear_env"
|
||||
),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(outcome) => Ok(outcome),
|
||||
Err(error) => Ok(ToolResult {
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(error.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::security::{AutonomyLevel, SecurityPolicy};
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn test_security() -> Arc<SecurityPolicy> {
|
||||
Arc::new(SecurityPolicy {
|
||||
autonomy: AutonomyLevel::Supervised,
|
||||
workspace_dir: std::env::temp_dir(),
|
||||
..SecurityPolicy::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn test_config(tmp: &TempDir) -> Arc<Config> {
|
||||
let config = Config {
|
||||
workspace_dir: tmp.path().join("workspace"),
|
||||
config_path: tmp.path().join("config.toml"),
|
||||
..Config::default()
|
||||
};
|
||||
config.save().unwrap();
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_services_action_returns_known_keys() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp), test_security());
|
||||
|
||||
let result = tool
|
||||
.execute(json!({"action": "list_services"}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.output.contains("provider.openai"));
|
||||
assert!(result.output.contains("tool.http_request"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_scope_services_requires_services_entries() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp), test_security());
|
||||
|
||||
let result = tool
|
||||
.execute(json!({
|
||||
"action": "set",
|
||||
"enabled": true,
|
||||
"scope": "services",
|
||||
"http_proxy": "http://127.0.0.1:7890",
|
||||
"services": []
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!result.success);
|
||||
assert!(result
|
||||
.error
|
||||
.unwrap_or_default()
|
||||
.contains("proxy.scope='services'"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_and_get_round_trip_proxy_scope() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let tool = ProxyConfigTool::new(test_config(&tmp), test_security());
|
||||
|
||||
let set_result = tool
|
||||
.execute(json!({
|
||||
"action": "set",
|
||||
"scope": "services",
|
||||
"http_proxy": "http://127.0.0.1:7890",
|
||||
"services": ["provider.openai", "tool.http_request"]
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(set_result.success, "{:?}", set_result.error);
|
||||
|
||||
let get_result = tool.execute(json!({"action": "get"})).await.unwrap();
|
||||
assert!(get_result.success);
|
||||
assert!(get_result.output.contains("provider.openai"));
|
||||
assert!(get_result.output.contains("services"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,21 @@
|
|||
use super::traits::{Tool, ToolResult};
|
||||
use crate::security::SecurityPolicy;
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
const PUSHOVER_API_URL: &str = "https://api.pushover.net/1/messages.json";
|
||||
const PUSHOVER_REQUEST_TIMEOUT_SECS: u64 = 15;
|
||||
|
||||
pub struct PushoverTool {
|
||||
client: Client,
|
||||
security: Arc<SecurityPolicy>,
|
||||
workspace_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl PushoverTool {
|
||||
pub fn new(security: Arc<SecurityPolicy>, workspace_dir: PathBuf) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(PUSHOVER_REQUEST_TIMEOUT_SECS))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new());
|
||||
|
||||
Self {
|
||||
client,
|
||||
security,
|
||||
workspace_dir,
|
||||
}
|
||||
|
|
@ -182,12 +173,12 @@ impl Tool for PushoverTool {
|
|||
form = form.text("sound", sound);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(PUSHOVER_API_URL)
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
let client = crate::config::build_runtime_proxy_client_with_timeouts(
|
||||
"tool.pushover",
|
||||
PUSHOVER_REQUEST_TIMEOUT_SECS,
|
||||
10,
|
||||
);
|
||||
let response = client.post(PUSHOVER_API_URL).multipart(form).send().await?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue