1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//! Stable error codes and user-actionable error shape.
use super::identifiers::AuditEventId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// Stable gateway error codes from the feature contract.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
/// Invalid configuration.
ConfigInvalid,
/// Broker base URL is missing.
ConfigMissingBrokerBaseUrl,
/// TLS bypass is configured for a non-localhost URL.
ConfigTlsBypassNonLocalhost,
/// Write tools are forbidden by current configuration.
ConfigWriteToolsForbidden,
/// Remote MCP is forbidden by current configuration.
ConfigRemoteMcpForbidden,
/// Sidecar relay is forbidden by current configuration.
ConfigSidecarForbidden,
/// Live trading is forbidden by current configuration.
ConfigLiveTradingForbidden,
/// Required scope is missing.
AuthMissingScope,
/// Bearer token is missing.
AuthTokenMissing,
/// Bearer token is invalid.
AuthTokenInvalid,
/// Bearer token is expired.
AuthTokenExpired,
/// Bearer token issuer is invalid.
AuthInvalidIssuer,
/// Bearer token audience/resource is invalid.
AuthInvalidAudience,
/// An unknown or disallowed scope was requested.
AuthScopeNotAllowedInMvp,
/// Only local config auth is allowed in the current mode.
AuthLocalOnlyMvp,
/// Account context is missing.
InputMissingAccount,
/// Account is not authorized.
InputUnauthorizedAccount,
/// Account selection is ambiguous.
InputAmbiguousAccount,
/// Contract resolution is ambiguous.
InputAmbiguousContract,
/// Asset class is unsupported.
InputUnsupportedAssetClass,
/// Contract id is invalid.
InputInvalidContract,
/// Time range is invalid.
InputInvalidTimeRange,
/// Broker login is required.
BrokerSessionRequired,
/// Broker session expired.
BrokerSessionExpired,
/// Broker backend is unavailable.
BrokerBackendUnavailable,
/// Broker rate limit was reached.
BrokerRateLimited,
/// Broker capability is unavailable.
BrokerCapabilityUnavailable,
/// Broker response could not be mapped safely.
BrokerResponseInvalid,
/// Market data is unavailable.
MarketDataUnavailable,
/// Market data is delayed.
MarketDataDelayed,
/// Market data is stale.
MarketDataStale,
/// Market data is incomplete.
MarketDataIncomplete,
/// Historical bars are unavailable.
HistoricalBarsUnavailable,
/// A write-like request was refused by read-only policy.
ReadonlyWriteForbidden,
/// Generic order preview is forbidden outside the explicit preview flow.
ReadonlyOrderPreviewForbidden,
/// Generic order submit is forbidden outside explicit paper/live flows.
ReadonlyOrderSubmitForbidden,
/// Generic order cancel is forbidden outside explicit paper/live flows.
ReadonlyOrderCancelForbidden,
/// Output was refused because it may expose unsafe material.
OutputUnsafe,
/// Audit write failed.
AuditWriteFailed,
/// Audit chain verification failed.
AuditChainInvalid,
/// Audit read scope is missing.
AuditReadForbidden,
/// Order preview is disabled by local configuration.
OrderPreviewDisabled,
/// Order policy refused the intent.
OrderPolicyRefused,
/// Order intent validation failed.
OrderValidationFailed,
/// Paper trading is disabled.
PaperTradingDisabled,
/// Paper order approval is missing or invalid.
PaperApprovalRequired,
/// Approval does not match the preview source of the submitted order.
ApprovalPreviewMismatch,
/// Approval was already consumed by a previous submit.
ApprovalConsumed,
/// Idempotency key is missing or conflicts with a prior request.
PaperIdempotencyConflict,
/// Sidecar pairing is missing or invalid.
SidecarPairingRequired,
/// Sidecar relay session is unavailable.
SidecarUnavailable,
/// Sidecar relay session is invalid or expired.
SidecarSessionInvalid,
/// Live trading is disabled.
LiveTradingDisabled,
/// A required live trading gate is missing.
LiveGateMissing,
/// Live hard-limit policy refused the order.
LiveLimitRefused,
/// Configured live policy id is unknown.
LivePolicyUnknown,
/// Live kill switch is closed.
LiveKillSwitchClosed,
/// Paper-to-live migration checklist is required.
LiveMigrationRequired,
}
/// Structured error returned by CLI, MCP, and service layers.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema, Error)]
#[error("{code:?}: {message}")]
pub struct GatewayError {
/// Stable error code.
pub code: ErrorCode,
/// User-actionable message that must not contain secrets.
pub message: String,
/// Whether retrying the same operation may succeed.
pub retryable: bool,
/// Optional safe user action.
pub user_action: Option<String>,
/// Optional audit event correlation.
pub audit_event_id: Option<AuditEventId>,
}
impl GatewayError {
/// Creates a new structured gateway error.
#[must_use]
pub fn new(
code: ErrorCode,
message: impl Into<String>,
retryable: bool,
user_action: Option<String>,
) -> Self {
Self {
code,
message: message.into(),
retryable,
user_action,
audit_event_id: None,
}
}
/// Adds audit event correlation to the error.
#[must_use]
pub fn with_audit_event_id(mut self, audit_event_id: AuditEventId) -> Self {
self.audit_event_id = Some(audit_event_id);
self
}
}