1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4pub fn binding_env_var(binding_name: &str) -> String {
9 alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16 #[error(
18 code = "KEY_INPUT_INVALID",
19 message = "Key input is invalid: {reason}",
20 retryable = "false",
21 internal = "false",
22 http_status_code = 400
23 )]
24 KeyInputInvalid { reason: String },
25
26 #[error(
28 code = "KEY_CIPHERTEXT_INVALID",
29 message = "Key ciphertext is invalid: {reason}",
30 retryable = "false",
31 internal = "false",
32 http_status_code = 400
33 )]
34 KeyCiphertextInvalid { reason: String },
35
36 #[error(
39 code = "BINDING_NOT_CONFIGURED",
40 message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
41 retryable = "false",
42 internal = "false",
43 http_status_code = 400
44 )]
45 BindingNotConfigured {
46 binding_name: String,
48 env_var: String,
50 },
51
52 #[error(
54 code = "BINDING_CONFIG_INVALID",
55 message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
56 retryable = "false",
57 internal = "false",
58 http_status_code = 400
59 )]
60 BindingConfigInvalid {
61 binding_name: String,
63 env_var: String,
65 reason: String,
67 },
68
69 #[error(
71 code = "UNSUPPORTED_BINDING_PROVIDER",
72 message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
73 retryable = "false",
74 internal = "false",
75 http_status_code = 501
76 )]
77 UnsupportedBindingProvider {
78 binding_name: String,
80 env_var: String,
82 provider: String,
84 },
85
86 #[error(
88 code = "STORAGE_OPERATION_FAILED",
89 message = "Storage operation failed for binding '{binding_name}': {operation}",
90 retryable = "true",
91 internal = "false",
92 http_status_code = 502
93 )]
94 StorageOperationFailed {
95 binding_name: String,
97 operation: String,
99 },
100
101 #[error(
103 code = "STORAGE_ACCESS_DENIED",
104 message = "Storage access denied for binding '{binding_name}' while attempting to {operation}",
105 retryable = "false",
106 internal = "false",
107 http_status_code = 403,
108 hint = "Check that the customer's Storage connection is active and its Access identity still has the required permissions."
109 )]
110 StorageAccessDenied {
111 binding_name: String,
113 operation: String,
115 },
116
117 #[error(
119 code = "STORAGE_OBJECT_NOT_FOUND",
120 message = "Storage object not found for binding '{binding_name}' while attempting to {operation}",
121 retryable = "false",
122 internal = "false",
123 http_status_code = 404
124 )]
125 StorageObjectNotFound {
126 binding_name: String,
128 operation: String,
130 },
131
132 #[error(
134 code = "BUILD_OPERATION_FAILED",
135 message = "Build operation failed for binding '{binding_name}': {operation}",
136 retryable = "true",
137 internal = "false",
138 http_status_code = 502
139 )]
140 BuildOperationFailed {
141 binding_name: String,
143 operation: String,
145 },
146
147 #[error(
149 code = "ENVIRONMENT_VARIABLE_MISSING",
150 message = "Required environment variable '{variable_name}' is missing",
151 retryable = "false",
152 internal = "false",
153 http_status_code = 500
154 )]
155 EnvironmentVariableMissing {
156 variable_name: String,
158 },
159
160 #[error(
162 code = "INVALID_ENVIRONMENT_VARIABLE",
163 message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
164 retryable = "false",
165 internal = "false",
166 http_status_code = 500
167 )]
168 InvalidEnvironmentVariable {
169 variable_name: String,
171 value: String,
173 reason: String,
175 },
176
177 #[error(
179 code = "INVALID_CONFIGURATION_URL",
180 message = "Invalid configuration URL '{url}': {reason}",
181 retryable = "false",
182 internal = "false",
183 http_status_code = 400
184 )]
185 InvalidConfigurationUrl {
186 url: String,
188 reason: String,
190 },
191
192 #[error(
194 code = "EVENT_PROCESSING_FAILED",
195 message = "Event processing failed for type '{event_type}': {reason}",
196 retryable = "true",
197 internal = "false",
198 http_status_code = 400
199 )]
200 EventProcessingFailed {
201 event_type: String,
203 reason: String,
205 },
206
207 #[error(
209 code = "GRPC_CONNECTION_FAILED",
210 message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
211 retryable = "true",
212 internal = "false",
213 http_status_code = 502
214 )]
215 GrpcConnectionFailed {
216 endpoint: String,
218 reason: String,
220 },
221
222 #[error(
224 code = "GRPC_SERVICE_UNAVAILABLE",
225 message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
226 retryable = "true",
227 internal = "false",
228 http_status_code = 503
229 )]
230 GrpcServiceUnavailable {
231 service: String,
233 endpoint: String,
235 reason: String,
237 },
238
239 #[error(
241 code = "GRPC_REQUEST_FAILED",
242 message = "gRPC request to service '{service}' method '{method}' failed: {details}",
243 retryable = "true",
244 internal = "false",
245 http_status_code = 502
246 )]
247 GrpcRequestFailed {
248 service: String,
250 method: String,
252 details: String,
254 },
255
256 #[error(
258 code = "SERVER_BIND_FAILED",
259 message = "Failed to bind server to address '{address}': {reason}",
260 retryable = "true",
261 internal = "true",
262 http_status_code = 500
263 )]
264 ServerBindFailed {
265 address: String,
267 reason: String,
269 },
270
271 #[error(
273 code = "AUTHENTICATION_FAILED",
274 message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
275 retryable = "true",
276 internal = "false",
277 http_status_code = 401
278 )]
279 AuthenticationFailed {
280 provider: String,
282 binding_name: String,
284 reason: String,
286 },
287
288 #[error(
290 code = "OPERATION_NOT_SUPPORTED",
291 message = "Operation '{operation}' not supported: {reason}",
292 retryable = "false",
293 internal = "false",
294 http_status_code = 501
295 )]
296 OperationNotSupported {
297 operation: String,
299 reason: String,
301 },
302
303 #[error(
309 code = "SANDBOX_COMMAND_FAILED",
310 message = "Sandbox command failed ({failure}): {reason}",
311 retryable = "false",
312 internal = "inherit",
313 http_status_code = 400
314 )]
315 SandboxCommandFailed {
316 failure: String,
318 reason: String,
320 },
321
322 #[error(
329 code = "SANDBOX_NOT_AS_DECLARED",
330 message = "Sandbox session '{session_id}' does not carry its declared {restriction}, so it cannot be used; create a new session. {reason}",
331 retryable = "false",
332 internal = "false",
333 http_status_code = 502
334 )]
335 SandboxNotAsDeclared {
336 session_id: String,
338 restriction: String,
340 reason: String,
342 },
343
344 #[error(
351 code = "SANDBOX_UNREACHABLE",
352 message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
353 retryable = "true",
354 internal = "inherit",
355 http_status_code = 503
356 )]
357 SandboxUnreachable {
358 operation: String,
360 reason: String,
362 },
363
364 #[error(
366 code = "FEATURE_NOT_ENABLED",
367 message = "Feature '{feature}' is not enabled in this build",
368 retryable = "false",
369 internal = "false",
370 http_status_code = 501
371 )]
372 FeatureNotEnabled {
373 feature: String,
375 },
376
377 #[error(
379 code = "GRPC_CALL_FAILED",
380 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
381 retryable = "true",
382 internal = "false",
383 http_status_code = 502
384 )]
385 GrpcCallFailed {
386 service: String,
388 method: String,
390 reason: String,
392 },
393
394 #[error(
396 code = "DESERIALIZATION_FAILED",
397 message = "Failed to deserialize {type_name}: {message}",
398 retryable = "false",
399 internal = "false",
400 http_status_code = 400
401 )]
402 DeserializationFailed {
403 message: String,
405 type_name: String,
407 },
408
409 #[error(
411 code = "SERIALIZATION_FAILED",
412 message = "Failed to serialize data: {message}",
413 retryable = "false",
414 internal = "false",
415 http_status_code = 500
416 )]
417 SerializationFailed {
418 message: String,
420 },
421
422 #[error(
424 code = "UNEXPECTED_RESPONSE_FORMAT",
425 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
426 retryable = "false",
427 internal = "false",
428 http_status_code = 502
429 )]
430 UnexpectedResponseFormat {
431 provider: String,
433 binding_name: String,
435 field: String,
437 response_json: String,
439 },
440
441 #[error(
443 code = "CLOUD_PLATFORM_ERROR",
444 message = "Cloud platform error: {message}",
445 retryable = "true",
446 internal = "false",
447 http_status_code = 502
448 )]
449 CloudPlatformError {
450 message: String,
452 resource_id: Option<String>,
454 },
455
456 #[error(
464 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
465 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
466 retryable = "inherit",
467 internal = "inherit",
468 http_status_code = 502
469 )]
470 PostgresSecretResolutionFailed {
471 binding_name: String,
473 secret: String,
475 reason: String,
477 },
478
479 #[error(
484 code = "POSTGRES_SECRET_VALUE_INVALID",
485 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
486 retryable = "false",
487 internal = "false",
488 http_status_code = 502
489 )]
490 PostgresSecretValueInvalid {
491 binding_name: String,
493 secret: String,
495 reason: String,
497 },
498
499 #[error(
501 code = "RESOURCE_NOT_FOUND",
502 message = "Resource '{resource_id}' not found",
503 retryable = "false",
504 internal = "false",
505 http_status_code = 404
506 )]
507 ResourceNotFound {
508 resource_id: String,
510 },
511
512 #[error(
514 code = "REMOTE_RESOURCE_NOT_FOUND",
515 message = "{operation_context}: {resource_type} '{resource_name}' not found",
516 retryable = "false",
517 internal = "false",
518 http_status_code = 404
519 )]
520 RemoteResourceNotFound {
521 operation_context: String,
523 resource_type: String,
525 resource_name: String,
527 },
528
529 #[error(
531 code = "REMOTE_RESOURCE_CONFLICT",
532 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
533 retryable = "true",
534 internal = "false",
535 http_status_code = 409
536 )]
537 RemoteResourceConflict {
538 operation_context: String,
540 resource_type: String,
542 resource_name: String,
544 conflict_reason: String,
546 },
547
548 #[error(
550 code = "REMOTE_ACCESS_DENIED",
551 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
552 retryable = "true",
553 internal = "false",
554 http_status_code = 403
555 )]
556 RemoteAccessDenied {
557 operation_context: String,
559 resource_type: String,
561 resource_name: String,
563 },
564
565 #[error(
567 code = "RATE_LIMIT_EXCEEDED",
568 message = "{operation_context}: Rate limit exceeded - {details}",
569 retryable = "true",
570 internal = "false",
571 http_status_code = 429
572 )]
573 RateLimitExceeded {
574 operation_context: String,
576 details: String,
578 },
579
580 #[error(
582 code = "TIMEOUT",
583 message = "{operation_context}: Operation timed out - {details}",
584 retryable = "true",
585 internal = "false",
586 http_status_code = 408
587 )]
588 Timeout {
589 operation_context: String,
591 details: String,
593 },
594
595 #[error(
597 code = "REMOTE_SERVICE_UNAVAILABLE",
598 message = "{operation_context}: Service unavailable - {details}",
599 retryable = "true",
600 internal = "false",
601 http_status_code = 503
602 )]
603 RemoteServiceUnavailable {
604 operation_context: String,
606 details: String,
608 },
609
610 #[error(
612 code = "QUOTA_EXCEEDED",
613 message = "{operation_context}: Quota exceeded - {details}",
614 retryable = "true",
615 internal = "false",
616 http_status_code = 429
617 )]
618 QuotaExceeded {
619 operation_context: String,
621 details: String,
623 },
624
625 #[error(
627 code = "INVALID_INPUT",
628 message = "{operation_context}: Invalid input - {details}",
629 retryable = "false",
630 internal = "false",
631 http_status_code = 400
632 )]
633 InvalidInput {
634 operation_context: String,
636 details: String,
638 field_name: Option<String>,
640 },
641
642 #[error(
644 code = "AUTHENTICATION_ERROR",
645 message = "{operation_context}: Authentication failed - {details}",
646 retryable = "true",
647 internal = "false",
648 http_status_code = 401
649 )]
650 AuthenticationError {
651 operation_context: String,
653 details: String,
655 },
656
657 #[error(
659 code = "BINDINGS_ERROR",
660 message = "Bindings error: {message}",
661 retryable = "true",
662 internal = "true",
663 http_status_code = 500
664 )]
665 Other {
666 message: String,
668 },
669
670 #[error(
672 code = "PRESIGNED_REQUEST_EXPIRED",
673 message = "Presigned request for path '{path}' expired at {expired_at}",
674 retryable = "false",
675 internal = "false",
676 http_status_code = 403
677 )]
678 PresignedRequestExpired {
679 path: String,
681 expired_at: chrono::DateTime<chrono::Utc>,
683 },
684
685 #[error(
687 code = "HTTP_REQUEST_FAILED",
688 message = "HTTP {method} request to '{url}' failed",
689 retryable = "true",
690 internal = "false",
691 http_status_code = 502
692 )]
693 HttpRequestFailed {
694 url: String,
696 method: String,
698 },
699
700 #[error(
702 code = "LOCAL_FILESYSTEM_ERROR",
703 message = "Local filesystem operation '{operation}' failed for path '{path}'",
704 retryable = "true",
705 internal = "false",
706 http_status_code = 500
707 )]
708 LocalFilesystemError {
709 path: String,
711 operation: String,
713 },
714
715 #[error(
717 code = "client_config_LOAD_FAILED",
718 message = "Failed to load platform configuration for provider '{provider}'",
719 retryable = "false",
720 internal = "false",
721 http_status_code = 400
722 )]
723 ClientConfigLoadFailed {
724 provider: String,
726 },
727
728 #[error(
730 code = "BINDING_SETUP_FAILED",
731 message = "Binding setup failed for type '{binding_type}': {reason}",
732 retryable = "false",
733 internal = "false",
734 http_status_code = 500
735 )]
736 BindingSetupFailed {
737 binding_type: String,
739 reason: String,
741 },
742
743 #[error(
745 code = "KV_OPERATION_FAILED",
746 message = "KV operation '{operation}' failed for key '{key}': {reason}",
747 retryable = "true",
748 internal = "false",
749 http_status_code = 502
750 )]
751 KvOperationFailed {
752 operation: String,
754 key: String,
756 reason: String,
758 },
759
760 #[error(
762 code = "QUEUE_OPERATION_FAILED",
763 message = "Queue operation '{operation}' failed: {reason}",
764 retryable = "true",
765 internal = "false",
766 http_status_code = 502
767 )]
768 QueueOperationFailed {
769 operation: String,
771 reason: String,
773 },
774
775 #[error(
777 code = "REMOTE_ACCESS_FAILED",
778 message = "Remote access failed during operation: {operation}",
779 retryable = "true",
780 internal = "false",
781 http_status_code = 502
782 )]
783 RemoteAccessFailed {
784 operation: String,
786 },
787
788 #[error(
790 code = "CLIENT_CONFIG_INVALID",
791 message = "Client configuration invalid for platform '{platform}': {message}",
792 retryable = "false",
793 internal = "false",
794 http_status_code = 400
795 )]
796 ClientConfigInvalid {
797 platform: alien_core::Platform,
799 message: String,
801 },
802}
803
804impl ErrorData {
805 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
809 ErrorData::BindingConfigInvalid {
810 env_var: binding_env_var(binding_name),
811 binding_name: binding_name.to_string(),
812 reason: reason.into(),
813 }
814 }
815
816 pub fn not_configured(binding_name: &str) -> Self {
819 ErrorData::BindingNotConfigured {
820 binding_name: binding_name.to_string(),
821 env_var: binding_env_var(binding_name),
822 }
823 }
824}
825
826pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
828
829pub type Error = AlienError<ErrorData>;
831
832pub fn map_cloud_client_error(
859 cloud_error: alien_client_core::Error,
860 operation_context: String,
861 resource_id: Option<String>,
862) -> Error {
863 use alien_client_core::ErrorData as CloudErrorData;
864
865 let error_data = match cloud_error.error.as_ref() {
867 Some(CloudErrorData::RemoteResourceNotFound {
868 resource_type,
869 resource_name,
870 }) => ErrorData::RemoteResourceNotFound {
871 operation_context,
872 resource_type: resource_type.clone(),
873 resource_name: resource_name.clone(),
874 },
875 Some(CloudErrorData::RemoteResourceConflict {
876 resource_type,
877 resource_name,
878 message,
879 }) => ErrorData::RemoteResourceConflict {
880 operation_context,
881 resource_type: resource_type.clone(),
882 resource_name: resource_name.clone(),
883 conflict_reason: message.clone(),
884 },
885 Some(CloudErrorData::RemoteAccessDenied {
886 resource_type,
887 resource_name,
888 }) => ErrorData::RemoteAccessDenied {
889 operation_context,
890 resource_type: resource_type.clone(),
891 resource_name: resource_name.clone(),
892 },
893 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
894 operation_context,
895 details: message.clone(),
896 },
897 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
898 operation_context,
899 details: message.clone(),
900 },
901 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
902 ErrorData::RemoteServiceUnavailable {
903 operation_context,
904 details: message.clone(),
905 }
906 }
907 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
908 operation_context,
909 details: message.clone(),
910 },
911 Some(CloudErrorData::InvalidInput {
912 message,
913 field_name,
914 }) => ErrorData::InvalidInput {
915 operation_context,
916 details: message.clone(),
917 field_name: field_name.clone(),
918 },
919 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
920 operation_context,
921 details: message.clone(),
922 },
923 _ => ErrorData::CloudPlatformError {
925 message: operation_context,
926 resource_id,
927 },
928 };
929
930 cloud_error.context(error_data)
932}