syncable_cli/agent/tools/platform/
check_provider_connection.rs1use 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#[derive(Debug, Deserialize)]
15pub struct CheckProviderConnectionArgs {
16 pub project_id: String,
18 pub provider: String,
20}
21
22#[derive(Debug, thiserror::Error)]
24#[error("Check provider connection error: {0}")]
25pub struct CheckProviderConnectionError(String);
26
27#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct CheckProviderConnectionTool;
34
35impl CheckProviderConnectionTool {
36 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 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 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 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 match client.check_provider_connection(&provider, &args.project_id).await {
132 Ok(Some(status)) => {
133 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 });
143
144 serde_json::to_string_pretty(&result)
145 .map_err(|e| CheckProviderConnectionError(format!("Failed to serialize: {}", e)))
146 }
147 Ok(None) => {
148 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
170fn 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}