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!("Invalid provider: '{}'. Must be one of: gcp, aws, azure, hetzner", args.provider),
112                    Some(vec![
113                        "Use 'gcp' for Google Cloud Platform",
114                        "Use 'aws' for Amazon Web Services",
115                        "Use 'azure' for Microsoft Azure",
116                        "Use 'hetzner' for Hetzner Cloud",
117                    ]),
118                ));
119            }
120        };
121
122        // Create the API client
123        let client = match PlatformApiClient::new() {
124            Ok(c) => c,
125            Err(e) => {
126                return Ok(format_api_error("check_provider_connection", e));
127            }
128        };
129
130        // Check the connection status
131        match client.check_provider_connection(&provider, &args.project_id).await {
132            Ok(Some(status)) => {
133                // Provider is connected
134                let result = json!({
135                    "connected": true,
136                    "provider": provider.as_str(),
137                    "provider_name": provider.display_name(),
138                    "project_id": args.project_id,
139                    "credential_id": status.id,
140                    "message": format!("{} is connected to this project", provider.display_name())
141                    // NOTE: We intentionally do NOT include any credential values here
142                });
143
144                serde_json::to_string_pretty(&result)
145                    .map_err(|e| CheckProviderConnectionError(format!("Failed to serialize: {}", e)))
146            }
147            Ok(None) => {
148                // Provider is NOT connected
149                let result = json!({
150                    "connected": false,
151                    "provider": provider.as_str(),
152                    "provider_name": provider.display_name(),
153                    "project_id": args.project_id,
154                    "message": format!("{} is NOT connected to this project", provider.display_name()),
155                    "next_steps": [
156                        "Use open_provider_settings to open the settings page",
157                        "Have the user connect their account in the browser",
158                        "Call check_provider_connection again to verify"
159                    ]
160                });
161
162                serde_json::to_string_pretty(&result)
163                    .map_err(|e| CheckProviderConnectionError(format!("Failed to serialize: {}", e)))
164            }
165            Err(e) => Ok(format_api_error("check_provider_connection", e)),
166        }
167    }
168}
169
170/// Format a PlatformApiError for LLM consumption
171fn format_api_error(tool_name: &str, error: PlatformApiError) -> String {
172    match error {
173        PlatformApiError::Unauthorized => format_error_for_llm(
174            tool_name,
175            ErrorCategory::PermissionDenied,
176            "Not authenticated - please run `sync-ctl auth login` first",
177            Some(vec![
178                "The user needs to authenticate with the Syncable platform",
179                "Run: sync-ctl auth login",
180            ]),
181        ),
182        PlatformApiError::NotFound(msg) => format_error_for_llm(
183            tool_name,
184            ErrorCategory::ResourceUnavailable,
185            &format!("Resource not found: {}", msg),
186            Some(vec![
187                "The project ID may be incorrect",
188                "Use list_projects to find valid project IDs",
189            ]),
190        ),
191        PlatformApiError::PermissionDenied(msg) => format_error_for_llm(
192            tool_name,
193            ErrorCategory::PermissionDenied,
194            &format!("Permission denied: {}", msg),
195            Some(vec![
196                "The user does not have access to this project",
197                "Contact the project admin for access",
198            ]),
199        ),
200        PlatformApiError::RateLimited => format_error_for_llm(
201            tool_name,
202            ErrorCategory::ResourceUnavailable,
203            "Rate limit exceeded - please try again later",
204            Some(vec!["Wait a moment before retrying"]),
205        ),
206        PlatformApiError::HttpError(e) => format_error_for_llm(
207            tool_name,
208            ErrorCategory::NetworkError,
209            &format!("Network error: {}", e),
210            Some(vec![
211                "Check network connectivity",
212                "The Syncable API may be temporarily unavailable",
213            ]),
214        ),
215        PlatformApiError::ParseError(msg) => format_error_for_llm(
216            tool_name,
217            ErrorCategory::InternalError,
218            &format!("Failed to parse API response: {}", msg),
219            Some(vec!["This may be a temporary API issue"]),
220        ),
221        PlatformApiError::ApiError { status, message } => format_error_for_llm(
222            tool_name,
223            ErrorCategory::ExternalCommandFailed,
224            &format!("API error ({}): {}", status, message),
225            Some(vec!["Check the error message for details"]),
226        ),
227        PlatformApiError::ServerError { status, message } => format_error_for_llm(
228            tool_name,
229            ErrorCategory::ExternalCommandFailed,
230            &format!("Server error ({}): {}", status, message),
231            Some(vec![
232                "The Syncable API is experiencing issues",
233                "Try again later",
234            ]),
235        ),
236        PlatformApiError::ConnectionFailed => format_error_for_llm(
237            tool_name,
238            ErrorCategory::NetworkError,
239            "Could not connect to Syncable API",
240            Some(vec![
241                "Check your internet connection",
242                "The Syncable API may be temporarily unavailable",
243            ]),
244        ),
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_tool_name() {
254        assert_eq!(CheckProviderConnectionTool::NAME, "check_provider_connection");
255    }
256
257    #[test]
258    fn test_tool_creation() {
259        let tool = CheckProviderConnectionTool::new();
260        assert!(format!("{:?}", tool).contains("CheckProviderConnectionTool"));
261    }
262
263    #[test]
264    fn test_provider_parsing() {
265        assert!("gcp".parse::<CloudProvider>().is_ok());
266        assert!("aws".parse::<CloudProvider>().is_ok());
267        assert!("azure".parse::<CloudProvider>().is_ok());
268        assert!("hetzner".parse::<CloudProvider>().is_ok());
269        assert!("invalid".parse::<CloudProvider>().is_err());
270    }
271}