use rig::completion::ToolDefinition;
use rig::tool::Tool;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
#[derive(Debug, Deserialize)]
pub struct OpenProviderSettingsArgs {
pub project_id: String,
}
#[derive(Debug, thiserror::Error)]
#[error("Open provider settings error: {0}")]
pub struct OpenProviderSettingsError(String);
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OpenProviderSettingsTool;
impl OpenProviderSettingsTool {
pub fn new() -> Self {
Self
}
}
impl Tool for OpenProviderSettingsTool {
const NAME: &'static str = "open_provider_settings";
type Error = OpenProviderSettingsError;
type Args = OpenProviderSettingsArgs;
type Output = String;
async fn definition(&self, _prompt: String) -> ToolDefinition {
ToolDefinition {
name: Self::NAME.to_string(),
description: r#"Open the cloud providers settings page in the user's browser.
This opens the Syncable platform's settings page where users can connect their
cloud provider accounts (GCP, AWS, Azure, Hetzner).
**Important:**
- The actual credential connection happens in the browser, NOT through the CLI
- After calling this tool, ask the user to confirm when they've completed the setup
- Use check_provider_connection to verify the connection was successful
**Workflow:**
1. Call open_provider_settings with the project_id
2. Ask user: "Please connect your [provider] account in the browser. Let me know when done."
3. Call check_provider_connection to verify the connection
**Prerequisites:**
- User must be authenticated via `sync-ctl auth login`
- User must have a valid project_id (from select_project or list_projects)"#
.to_string(),
parameters: json!({
"type": "object",
"properties": {
"project_id": {
"type": "string",
"description": "The UUID of the project to configure cloud providers for"
}
},
"required": ["project_id"]
}),
}
}
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
if args.project_id.trim().is_empty() {
return Ok(format_error_for_llm(
"open_provider_settings",
ErrorCategory::ValidationFailed,
"project_id cannot be empty",
Some(vec![
"Use list_projects to find valid project IDs",
"Use select_project to set the current project context",
]),
));
}
let url = format!(
"https://syncable.dev/projects/{}/settings?tab=cloud-providers",
args.project_id
);
match open::that(&url) {
Ok(()) => {
let result = json!({
"success": true,
"message": "Opened cloud providers settings in your browser",
"url": url,
"next_steps": [
"Connect your cloud provider account in the browser",
"Once done, tell me which provider you connected",
"I'll verify the connection with check_provider_connection"
]
});
serde_json::to_string_pretty(&result)
.map_err(|e| OpenProviderSettingsError(format!("Failed to serialize: {}", e)))
}
Err(e) => Ok(format_error_for_llm(
"open_provider_settings",
ErrorCategory::ExternalCommandFailed,
&format!("Failed to open browser: {}", e),
Some(vec![
&format!("You can manually open: {}", url),
"Check if a default browser is configured",
]),
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_name() {
assert_eq!(OpenProviderSettingsTool::NAME, "open_provider_settings");
}
#[test]
fn test_tool_creation() {
let tool = OpenProviderSettingsTool::new();
assert!(format!("{:?}", tool).contains("OpenProviderSettingsTool"));
}
#[test]
fn test_settings_url_format() {
let project_id = "proj-12345-uuid";
let expected_url = format!(
"https://syncable.dev/projects/{}/settings?tab=cloud-providers",
project_id
);
assert!(expected_url.contains(project_id));
assert!(expected_url.contains("cloud-providers"));
}
}