syncable_cli/agent/tools/platform/
open_provider_settings.rs

1//! Open provider settings tool for the agent
2//!
3//! Opens the cloud providers settings page in the user's browser.
4
5use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9
10use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
11
12/// Arguments for the open provider settings tool
13#[derive(Debug, Deserialize)]
14pub struct OpenProviderSettingsArgs {
15    /// The project ID to open settings for
16    pub project_id: String,
17}
18
19/// Error type for open provider settings operations
20#[derive(Debug, thiserror::Error)]
21#[error("Open provider settings error: {0}")]
22pub struct OpenProviderSettingsError(String);
23
24/// Tool to open the cloud providers settings page in the browser
25///
26/// This tool opens the Syncable platform's cloud providers settings page
27/// where users can connect their GCP, AWS, Azure, or Hetzner accounts.
28///
29/// SECURITY NOTE: The actual credential connection happens entirely in the
30/// browser through the platform's secure OAuth flow. The CLI agent NEVER
31/// handles or sees the actual credentials.
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct OpenProviderSettingsTool;
34
35impl OpenProviderSettingsTool {
36    /// Create a new OpenProviderSettingsTool
37    pub fn new() -> Self {
38        Self
39    }
40}
41
42impl Tool for OpenProviderSettingsTool {
43    const NAME: &'static str = "open_provider_settings";
44
45    type Error = OpenProviderSettingsError;
46    type Args = OpenProviderSettingsArgs;
47    type Output = String;
48
49    async fn definition(&self, _prompt: String) -> ToolDefinition {
50        ToolDefinition {
51            name: Self::NAME.to_string(),
52            description: r#"Open the cloud providers settings page in the user's browser.
53
54This opens the Syncable platform's settings page where users can connect their
55cloud provider accounts (GCP, AWS, Azure, Hetzner).
56
57**Important:**
58- The actual credential connection happens in the browser, NOT through the CLI
59- After calling this tool, ask the user to confirm when they've completed the setup
60- Use check_provider_connection to verify the connection was successful
61
62**Workflow:**
631. Call open_provider_settings with the project_id
642. Ask user: "Please connect your [provider] account in the browser. Let me know when done."
653. Call check_provider_connection to verify the connection
66
67**Prerequisites:**
68- User must be authenticated via `sync-ctl auth login`
69- User must have a valid project_id (from select_project or list_projects)"#
70                .to_string(),
71            parameters: json!({
72                "type": "object",
73                "properties": {
74                    "project_id": {
75                        "type": "string",
76                        "description": "The UUID of the project to configure cloud providers for"
77                    }
78                },
79                "required": ["project_id"]
80            }),
81        }
82    }
83
84    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
85        // Validate input
86        if args.project_id.trim().is_empty() {
87            return Ok(format_error_for_llm(
88                "open_provider_settings",
89                ErrorCategory::ValidationFailed,
90                "project_id cannot be empty",
91                Some(vec![
92                    "Use list_projects to find valid project IDs",
93                    "Use select_project to set the current project context",
94                ]),
95            ));
96        }
97
98        // Build the settings URL
99        let url = format!(
100            "https://syncable.dev/projects/{}/settings?tab=cloud-providers",
101            args.project_id
102        );
103
104        // Open the URL in the default browser
105        match open::that(&url) {
106            Ok(()) => {
107                let result = json!({
108                    "success": true,
109                    "message": "Opened cloud providers settings in your browser",
110                    "url": url,
111                    "next_steps": [
112                        "Connect your cloud provider account in the browser",
113                        "Once done, tell me which provider you connected",
114                        "I'll verify the connection with check_provider_connection"
115                    ]
116                });
117
118                serde_json::to_string_pretty(&result)
119                    .map_err(|e| OpenProviderSettingsError(format!("Failed to serialize: {}", e)))
120            }
121            Err(e) => Ok(format_error_for_llm(
122                "open_provider_settings",
123                ErrorCategory::ExternalCommandFailed,
124                &format!("Failed to open browser: {}", e),
125                Some(vec![
126                    &format!("You can manually open: {}", url),
127                    "Check if a default browser is configured",
128                ]),
129            )),
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_tool_name() {
140        assert_eq!(OpenProviderSettingsTool::NAME, "open_provider_settings");
141    }
142
143    #[test]
144    fn test_tool_creation() {
145        let tool = OpenProviderSettingsTool::new();
146        assert!(format!("{:?}", tool).contains("OpenProviderSettingsTool"));
147    }
148
149    #[test]
150    fn test_settings_url_format() {
151        let project_id = "proj-12345-uuid";
152        let expected_url = format!(
153            "https://syncable.dev/projects/{}/settings?tab=cloud-providers",
154            project_id
155        );
156        assert!(expected_url.contains(project_id));
157        assert!(expected_url.contains("cloud-providers"));
158    }
159}