Skip to main content

syncable_cli/agent/tools/platform/
check_provider_connection.rs

1//! Check provider connection tool for the agent
2//!
3//! Checks if a cloud provider is connected to a project.
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};
11use crate::platform::api::{CloudProvider, PlatformApiClient, PlatformApiError};
12
13/// Arguments for the check provider connection tool
14#[derive(Debug, Deserialize)]
15pub struct CheckProviderConnectionArgs {
16    /// The project ID to check
17    pub project_id: String,
18    /// The cloud provider to check (gcp, aws, azure, hetzner)
19    pub provider: String,
20}
21
22/// Error type for check provider connection operations
23#[derive(Debug, thiserror::Error)]
24#[error("Check provider connection error: {0}")]
25pub struct CheckProviderConnectionError(String);
26
27/// Tool to check if a cloud provider is connected to a project
28///
29/// SECURITY NOTE: This tool only returns connection STATUS (connected/not connected).
30/// It NEVER returns actual credentials, tokens, or API keys. The agent should never
31/// have access to sensitive authentication material.
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct CheckProviderConnectionTool;
34
35impl CheckProviderConnectionTool {
36    /// Create a new CheckProviderConnectionTool
37    pub fn new() -> Self {
38        Self
39    }
40}
41
42impl Tool for CheckProviderConnectionTool {
43    const NAME: &'static str = "check_provider_connection";
44
45    type Error = CheckProviderConnectionError;
46    type Args = CheckProviderConnectionArgs;
47    type Output = String;
48
49    async fn definition(&self, _prompt: String) -> ToolDefinition {
50        ToolDefinition {
51            name: Self::NAME.to_string(),
52            description: r#"Check if a cloud provider is connected to a project.
53
54Returns connection status (connected or not connected) for the specified provider.
55This tool NEVER returns actual credentials - only connection status.
56
57**Supported Providers:**
58- gcp (Google Cloud Platform)
59- aws (Amazon Web Services)
60- azure (Microsoft Azure)
61- hetzner (Hetzner Cloud)
62
63**Use Cases:**
64- Verify a provider was connected after user completes setup in browser
65- Check prerequisites before deployment operations
66- Determine which providers are available for a project
67
68**Prerequisites:**
69- User must be authenticated via `sync-ctl auth login`
70- A project must be selected (use select_project first)"#
71                .to_string(),
72            parameters: json!({
73                "type": "object",
74                "properties": {
75                    "project_id": {
76                        "type": "string",
77                        "description": "The UUID of the project to check"
78                    },
79                    "provider": {
80                        "type": "string",
81                        "enum": ["gcp", "aws", "azure", "hetzner"],
82                        "description": "The cloud provider to check: gcp, aws, azure, or hetzner"
83                    }
84                },
85                "required": ["project_id", "provider"]
86            }),
87        }
88    }
89
90    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
91        // Validate project_id
92        if args.project_id.trim().is_empty() {
93            return Ok(format_error_for_llm(
94                "check_provider_connection",
95                ErrorCategory::ValidationFailed,
96                "project_id cannot be empty",
97                Some(vec![
98                    "Use list_projects to find valid project IDs",
99                    "Use select_project to set the current project context",
100                ]),
101            ));
102        }
103
104        // Parse and validate provider
105        let provider: CloudProvider = match args.provider.parse() {
106            Ok(p) => p,
107            Err(_) => {
108                return Ok(format_error_for_llm(
109                    "check_provider_connection",
110                    ErrorCategory::ValidationFailed,
111                    &format!(
112                        "Invalid provider: '{}'. Must be one of: gcp, aws, azure, hetzner",
113                        args.provider
114                    ),
115                    Some(vec![
116                        "Use 'gcp' for Google Cloud Platform",
117                        "Use 'aws' for Amazon Web Services",
118                        "Use 'azure' for Microsoft Azure",
119                        "Use 'hetzner' for Hetzner Cloud",
120                    ]),
121                ));
122            }
123        };
124
125        // Create the API client
126        let client = match PlatformApiClient::new() {
127            Ok(c) => c,
128            Err(e) => {
129                return Ok(format_api_error("check_provider_connection", e));
130            }
131        };
132
133        // Check the connection status
134        match client
135            .check_provider_connection(&provider, &args.project_id)
136            .await
137        {
138            Ok(Some(status)) => {
139                // Provider is connected
140                let result = json!({
141                    "connected": true,
142                    "provider": provider.as_str(),
143                    "provider_name": provider.display_name(),
144                    "project_id": args.project_id,
145                    "credential_id": status.id,
146                    "message": format!("{} is connected to this project", provider.display_name())
147                    // NOTE: We intentionally do NOT include any credential values here
148                });
149
150                serde_json::to_string_pretty(&result).map_err(|e| {
151                    CheckProviderConnectionError(format!("Failed to serialize: {}", e))
152                })
153            }
154            Ok(None) => {
155                // Provider is NOT connected
156                let result = json!({
157                    "connected": false,
158                    "provider": provider.as_str(),
159                    "provider_name": provider.display_name(),
160                    "project_id": args.project_id,
161                    "message": format!("{} is NOT connected to this project", provider.display_name()),
162                    "next_steps": [
163                        "Use open_provider_settings to open the settings page",
164                        "Have the user connect their account in the browser",
165                        "Call check_provider_connection again to verify"
166                    ]
167                });
168
169                serde_json::to_string_pretty(&result).map_err(|e| {
170                    CheckProviderConnectionError(format!("Failed to serialize: {}", e))
171                })
172            }
173            Err(e) => Ok(format_api_error("check_provider_connection", e)),
174        }
175    }
176}
177
178/// Format a PlatformApiError for LLM consumption
179fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
180    match error {
181        PlatformApiError::Unauthorized => format_error_for_llm(
182            tool_name,
183            ErrorCategory::PermissionDenied,
184            "Not authenticated - please run `sync-ctl auth login` first",
185            Some(vec![
186                "The user needs to authenticate with the Syncable platform",
187                "Run: sync-ctl auth login",
188            ]),
189        ),
190        PlatformApiError::NotFound(msg) => format_error_for_llm(
191            tool_name,
192            ErrorCategory::ResourceUnavailable,
193            &format!("Resource not found: {}", msg),
194            Some(vec![
195                "The project ID may be incorrect",
196                "Use list_projects to find valid project IDs",
197            ]),
198        ),
199        PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
200            tool_name,
201            ErrorCategory::PermissionDenied,
202            &format!("Permission denied: {}", msg),
203            Some(vec![
204                "The user does not have access to this project",
205                "Contact the project admin for access",
206            ]),
207        ),
208        PlatformApiError::RateLimited => format_error_for_llm(
209            tool_name,
210            ErrorCategory::ResourceUnavailable,
211            "Rate limit exceeded - please try again later",
212            Some(vec!["Wait a moment before retrying"]),
213        ),
214        PlatformApiError::HttpError(e) => format_error_for_llm(
215            tool_name,
216            ErrorCategory::NetworkError,
217            &format!("Network error: {}", e),
218            Some(vec![
219                "Check network connectivity",
220                "The Syncable API may be temporarily unavailable",
221            ]),
222        ),
223        PlatformApiError::ParseError(msg) => format_error_for_llm(
224            tool_name,
225            ErrorCategory::InternalError,
226            &format!("Failed to parse API response: {}", msg),
227            Some(vec!["This may be a temporary API issue"]),
228        ),
229        PlatformApiError::ApiError { status, message } => format_error_for_llm(
230            tool_name,
231            ErrorCategory::ExternalCommandFailed,
232            &format!("API error ({}): {}", status, message),
233            Some(vec!["Check the error message for details"]),
234        ),
235        PlatformApiError::ServerError { status, message } => format_error_for_llm(
236            tool_name,
237            ErrorCategory::ExternalCommandFailed,
238            &format!("Server error ({}): {}", status, message),
239            Some(vec![
240                "The Syncable API is experiencing issues",
241                "Try again later",
242            ]),
243        ),
244        PlatformApiError::ConnectionFailed => format_error_for_llm(
245            tool_name,
246            ErrorCategory::NetworkError,
247            "Could not connect to Syncable API",
248            Some(vec![
249                "Check your internet connection",
250                "The Syncable API may be temporarily unavailable",
251            ]),
252        ),
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_tool_name() {
262        assert_eq!(
263            CheckProviderConnectionTool::NAME,
264            "check_provider_connection"
265        );
266    }
267
268    #[test]
269    fn test_tool_creation() {
270        let tool = CheckProviderConnectionTool::new();
271        assert!(format!("{:?}", tool).contains("CheckProviderConnectionTool"));
272    }
273
274    #[test]
275    fn test_provider_parsing() {
276        assert!("gcp".parse::<CloudProvider>().is_ok());
277        assert!("aws".parse::<CloudProvider>().is_ok());
278        assert!("azure".parse::<CloudProvider>().is_ok());
279        assert!("hetzner".parse::<CloudProvider>().is_ok());
280        assert!("invalid".parse::<CloudProvider>().is_err());
281    }
282}