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,
319 reason: String,
321 },
322
323 #[error(
332 code = "SANDBOX_OUTCOME_UNKNOWN",
333 message = "Sandbox operation '{operation}' did not report its outcome and may have taken effect: {reason}",
334 retryable = "false",
335 internal = "inherit",
336 http_status_code = 502
337 )]
338 SandboxOutcomeUnknown {
339 operation: String,
341 reason: String,
343 },
344
345 #[error(
352 code = "SANDBOX_NOT_AS_DECLARED",
353 message = "Sandbox session '{session_id}' does not carry its declared {restriction}, so it cannot be used; create a new session. {reason}",
354 retryable = "false",
355 internal = "false",
356 http_status_code = 502
357 )]
358 SandboxNotAsDeclared {
359 session_id: String,
361 restriction: String,
363 reason: String,
365 },
366
367 #[error(
374 code = "SANDBOX_UNREACHABLE",
375 message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
376 retryable = "true",
377 internal = "inherit",
378 http_status_code = 503
379 )]
380 SandboxUnreachable {
381 operation: String,
383 reason: String,
385 },
386
387 #[error(
389 code = "FEATURE_NOT_ENABLED",
390 message = "Feature '{feature}' is not enabled in this build",
391 retryable = "false",
392 internal = "false",
393 http_status_code = 501
394 )]
395 FeatureNotEnabled {
396 feature: String,
398 },
399
400 #[error(
402 code = "GRPC_CALL_FAILED",
403 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
404 retryable = "true",
405 internal = "false",
406 http_status_code = 502
407 )]
408 GrpcCallFailed {
409 service: String,
411 method: String,
413 reason: String,
415 },
416
417 #[error(
419 code = "DESERIALIZATION_FAILED",
420 message = "Failed to deserialize {type_name}: {message}",
421 retryable = "false",
422 internal = "false",
423 http_status_code = 400
424 )]
425 DeserializationFailed {
426 message: String,
428 type_name: String,
430 },
431
432 #[error(
434 code = "SERIALIZATION_FAILED",
435 message = "Failed to serialize data: {message}",
436 retryable = "false",
437 internal = "false",
438 http_status_code = 500
439 )]
440 SerializationFailed {
441 message: String,
443 },
444
445 #[error(
447 code = "UNEXPECTED_RESPONSE_FORMAT",
448 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
449 retryable = "false",
450 internal = "false",
451 http_status_code = 502
452 )]
453 UnexpectedResponseFormat {
454 provider: String,
456 binding_name: String,
458 field: String,
460 response_json: String,
462 },
463
464 #[error(
466 code = "CLOUD_PLATFORM_ERROR",
467 message = "Cloud platform error: {message}",
468 retryable = "true",
469 internal = "false",
470 http_status_code = 502
471 )]
472 CloudPlatformError {
473 message: String,
475 resource_id: Option<String>,
477 },
478
479 #[error(
487 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
488 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
489 retryable = "inherit",
490 internal = "inherit",
491 http_status_code = 502
492 )]
493 PostgresSecretResolutionFailed {
494 binding_name: String,
496 secret: String,
498 reason: String,
500 },
501
502 #[error(
507 code = "POSTGRES_SECRET_VALUE_INVALID",
508 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
509 retryable = "false",
510 internal = "false",
511 http_status_code = 502
512 )]
513 PostgresSecretValueInvalid {
514 binding_name: String,
516 secret: String,
518 reason: String,
520 },
521
522 #[error(
524 code = "RESOURCE_NOT_FOUND",
525 message = "Resource '{resource_id}' not found",
526 retryable = "false",
527 internal = "false",
528 http_status_code = 404
529 )]
530 ResourceNotFound {
531 resource_id: String,
533 },
534
535 #[error(
537 code = "REMOTE_RESOURCE_NOT_FOUND",
538 message = "{operation_context}: {resource_type} '{resource_name}' not found",
539 retryable = "false",
540 internal = "false",
541 http_status_code = 404
542 )]
543 RemoteResourceNotFound {
544 operation_context: String,
546 resource_type: String,
548 resource_name: String,
550 },
551
552 #[error(
554 code = "REMOTE_RESOURCE_CONFLICT",
555 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
556 retryable = "true",
557 internal = "false",
558 http_status_code = 409
559 )]
560 RemoteResourceConflict {
561 operation_context: String,
563 resource_type: String,
565 resource_name: String,
567 conflict_reason: String,
569 },
570
571 #[error(
573 code = "REMOTE_ACCESS_DENIED",
574 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
575 retryable = "true",
576 internal = "false",
577 http_status_code = 403
578 )]
579 RemoteAccessDenied {
580 operation_context: String,
582 resource_type: String,
584 resource_name: String,
586 },
587
588 #[error(
590 code = "RATE_LIMIT_EXCEEDED",
591 message = "{operation_context}: Rate limit exceeded - {details}",
592 retryable = "true",
593 internal = "false",
594 http_status_code = 429
595 )]
596 RateLimitExceeded {
597 operation_context: String,
599 details: String,
601 },
602
603 #[error(
605 code = "TIMEOUT",
606 message = "{operation_context}: Operation timed out - {details}",
607 retryable = "true",
608 internal = "false",
609 http_status_code = 408
610 )]
611 Timeout {
612 operation_context: String,
614 details: String,
616 },
617
618 #[error(
620 code = "REMOTE_SERVICE_UNAVAILABLE",
621 message = "{operation_context}: Service unavailable - {details}",
622 retryable = "true",
623 internal = "false",
624 http_status_code = 503
625 )]
626 RemoteServiceUnavailable {
627 operation_context: String,
629 details: String,
631 },
632
633 #[error(
635 code = "QUOTA_EXCEEDED",
636 message = "{operation_context}: Quota exceeded - {details}",
637 retryable = "true",
638 internal = "false",
639 http_status_code = 429
640 )]
641 QuotaExceeded {
642 operation_context: String,
644 details: String,
646 },
647
648 #[error(
650 code = "INVALID_INPUT",
651 message = "{operation_context}: Invalid input - {details}",
652 retryable = "false",
653 internal = "false",
654 http_status_code = 400
655 )]
656 InvalidInput {
657 operation_context: String,
659 details: String,
661 field_name: Option<String>,
663 },
664
665 #[error(
667 code = "AUTHENTICATION_ERROR",
668 message = "{operation_context}: Authentication failed - {details}",
669 retryable = "true",
670 internal = "false",
671 http_status_code = 401
672 )]
673 AuthenticationError {
674 operation_context: String,
676 details: String,
678 },
679
680 #[error(
682 code = "BINDINGS_ERROR",
683 message = "Bindings error: {message}",
684 retryable = "true",
685 internal = "true",
686 http_status_code = 500
687 )]
688 Other {
689 message: String,
691 },
692
693 #[error(
695 code = "PRESIGNED_REQUEST_EXPIRED",
696 message = "Presigned request for path '{path}' expired at {expired_at}",
697 retryable = "false",
698 internal = "false",
699 http_status_code = 403
700 )]
701 PresignedRequestExpired {
702 path: String,
704 expired_at: chrono::DateTime<chrono::Utc>,
706 },
707
708 #[error(
710 code = "HTTP_REQUEST_FAILED",
711 message = "HTTP {method} request to '{url}' failed",
712 retryable = "true",
713 internal = "false",
714 http_status_code = 502
715 )]
716 HttpRequestFailed {
717 url: String,
719 method: String,
721 },
722
723 #[error(
725 code = "LOCAL_FILESYSTEM_ERROR",
726 message = "Local filesystem operation '{operation}' failed for path '{path}'",
727 retryable = "true",
728 internal = "false",
729 http_status_code = 500
730 )]
731 LocalFilesystemError {
732 path: String,
734 operation: String,
736 },
737
738 #[error(
740 code = "client_config_LOAD_FAILED",
741 message = "Failed to load platform configuration for provider '{provider}'",
742 retryable = "false",
743 internal = "false",
744 http_status_code = 400
745 )]
746 ClientConfigLoadFailed {
747 provider: String,
749 },
750
751 #[error(
753 code = "BINDING_SETUP_FAILED",
754 message = "Binding setup failed for type '{binding_type}': {reason}",
755 retryable = "false",
756 internal = "false",
757 http_status_code = 500
758 )]
759 BindingSetupFailed {
760 binding_type: String,
762 reason: String,
764 },
765
766 #[error(
768 code = "KV_OPERATION_FAILED",
769 message = "KV operation '{operation}' failed for key '{key}': {reason}",
770 retryable = "true",
771 internal = "false",
772 http_status_code = 502
773 )]
774 KvOperationFailed {
775 operation: String,
777 key: String,
779 reason: String,
781 },
782
783 #[error(
785 code = "QUEUE_OPERATION_FAILED",
786 message = "Queue operation '{operation}' failed: {reason}",
787 retryable = "true",
788 internal = "false",
789 http_status_code = 502
790 )]
791 QueueOperationFailed {
792 operation: String,
794 reason: String,
796 },
797
798 #[error(
800 code = "REMOTE_ACCESS_FAILED",
801 message = "Remote access failed during operation: {operation}",
802 retryable = "true",
803 internal = "false",
804 http_status_code = 502
805 )]
806 RemoteAccessFailed {
807 operation: String,
809 },
810
811 #[error(
813 code = "CLIENT_CONFIG_INVALID",
814 message = "Client configuration invalid for platform '{platform}': {message}",
815 retryable = "false",
816 internal = "false",
817 http_status_code = 400
818 )]
819 ClientConfigInvalid {
820 platform: alien_core::Platform,
822 message: String,
824 },
825}
826
827impl ErrorData {
828 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
832 ErrorData::BindingConfigInvalid {
833 env_var: binding_env_var(binding_name),
834 binding_name: binding_name.to_string(),
835 reason: reason.into(),
836 }
837 }
838
839 pub fn not_configured(binding_name: &str) -> Self {
842 ErrorData::BindingNotConfigured {
843 binding_name: binding_name.to_string(),
844 env_var: binding_env_var(binding_name),
845 }
846 }
847}
848
849pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
851
852pub type Error = AlienError<ErrorData>;
854
855pub fn map_cloud_client_error(
882 cloud_error: alien_client_core::Error,
883 operation_context: String,
884 resource_id: Option<String>,
885) -> Error {
886 use alien_client_core::ErrorData as CloudErrorData;
887
888 let error_data = match cloud_error.error.as_ref() {
890 Some(CloudErrorData::RemoteResourceNotFound {
891 resource_type,
892 resource_name,
893 }) => ErrorData::RemoteResourceNotFound {
894 operation_context,
895 resource_type: resource_type.clone(),
896 resource_name: resource_name.clone(),
897 },
898 Some(CloudErrorData::RemoteResourceConflict {
899 resource_type,
900 resource_name,
901 message,
902 }) => ErrorData::RemoteResourceConflict {
903 operation_context,
904 resource_type: resource_type.clone(),
905 resource_name: resource_name.clone(),
906 conflict_reason: message.clone(),
907 },
908 Some(CloudErrorData::RemoteAccessDenied {
909 resource_type,
910 resource_name,
911 }) => ErrorData::RemoteAccessDenied {
912 operation_context,
913 resource_type: resource_type.clone(),
914 resource_name: resource_name.clone(),
915 },
916 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
917 operation_context,
918 details: message.clone(),
919 },
920 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
921 operation_context,
922 details: message.clone(),
923 },
924 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
925 ErrorData::RemoteServiceUnavailable {
926 operation_context,
927 details: message.clone(),
928 }
929 }
930 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
931 operation_context,
932 details: message.clone(),
933 },
934 Some(CloudErrorData::InvalidInput {
935 message,
936 field_name,
937 }) => ErrorData::InvalidInput {
938 operation_context,
939 details: message.clone(),
940 field_name: field_name.clone(),
941 },
942 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
943 operation_context,
944 details: message.clone(),
945 },
946 _ => ErrorData::CloudPlatformError {
948 message: operation_context,
949 resource_id,
950 },
951 };
952
953 cloud_error.context(error_data)
955}