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!(
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 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 match client
135 .check_provider_connection(&provider, &args.project_id)
136 .await
137 {
138 Ok(Some(status)) => {
139 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 });
149
150 serde_json::to_string_pretty(&result).map_err(|e| {
151 CheckProviderConnectionError(format!("Failed to serialize: {}", e))
152 })
153 }
154 Ok(None) => {
155 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
178fn 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}