1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use thiserror::Error;
6use uuid::Uuid;
7
8use crate::{
9 ArtifactId, Capability, CapabilitySet, CommandId, CommandOutcome, ErrorLayer, PrincipalId,
10};
11
12pub const CURRENT_INTERFACE_VERSION: &str = "2026-07-23";
14
15#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18pub struct InterfaceVersion;
19
20impl InterfaceVersion {
21 pub const CURRENT: Self = Self;
22}
23
24impl TryFrom<&str> for InterfaceVersion {
25 type Error = InterfaceValidationError;
26
27 fn try_from(value: &str) -> Result<Self, Self::Error> {
28 if value == CURRENT_INTERFACE_VERSION {
29 Ok(Self)
30 } else {
31 Err(InterfaceValidationError::UnsupportedInterfaceVersion(
32 value.to_owned(),
33 ))
34 }
35 }
36}
37
38impl TryFrom<String> for InterfaceVersion {
39 type Error = InterfaceValidationError;
40
41 fn try_from(value: String) -> Result<Self, Self::Error> {
42 Self::try_from(value.as_str())
43 }
44}
45
46impl Serialize for InterfaceVersion {
47 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
48 where
49 S: Serializer,
50 {
51 serializer.serialize_str(CURRENT_INTERFACE_VERSION)
52 }
53}
54
55impl<'de> Deserialize<'de> for InterfaceVersion {
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: Deserializer<'de>,
59 {
60 let value = String::deserialize(deserializer)?;
61 Self::try_from(value).map_err(serde::de::Error::custom)
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[serde(transparent)]
69pub struct CorrelationId(Uuid);
70
71impl CorrelationId {
72 pub fn new() -> Self {
73 Self(Uuid::new_v4())
74 }
75
76 pub fn from_uuid(value: Uuid) -> Self {
77 Self(value)
78 }
79
80 pub fn as_uuid(&self) -> Uuid {
81 self.0
82 }
83}
84
85impl Default for CorrelationId {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
93#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
94pub struct IdempotencyKey(String);
95
96impl IdempotencyKey {
97 pub fn as_str(&self) -> &str {
98 &self.0
99 }
100}
101
102impl TryFrom<&str> for IdempotencyKey {
103 type Error = InterfaceValidationError;
104
105 fn try_from(value: &str) -> Result<Self, Self::Error> {
106 if !(1..=128).contains(&value.len())
107 || !value
108 .as_bytes()
109 .iter()
110 .all(|byte| (0x20..=0x7e).contains(byte))
111 {
112 return Err(InterfaceValidationError::InvalidIdempotencyKey);
113 }
114 Ok(Self(value.to_owned()))
115 }
116}
117
118impl TryFrom<String> for IdempotencyKey {
119 type Error = InterfaceValidationError;
120
121 fn try_from(value: String) -> Result<Self, Self::Error> {
122 Self::try_from(value.as_str())
123 }
124}
125
126impl Serialize for IdempotencyKey {
127 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
128 where
129 S: Serializer,
130 {
131 serializer.serialize_str(&self.0)
132 }
133}
134
135impl<'de> Deserialize<'de> for IdempotencyKey {
136 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137 where
138 D: Deserializer<'de>,
139 {
140 let value = String::deserialize(deserializer)?;
141 Self::try_from(value).map_err(serde::de::Error::custom)
142 }
143}
144
145#[derive(
146 Debug, Clone, Copy, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize,
147)]
148#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
149#[serde(transparent)]
150pub struct EventCursor(pub u64);
151
152impl EventCursor {
153 pub const ZERO: Self = Self(0);
154}
155
156impl From<u64> for EventCursor {
157 fn from(value: u64) -> Self {
158 Self(value)
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
164#[serde(rename_all = "camelCase")]
165pub struct RequestContext {
166 pub interface_version: InterfaceVersion,
167 pub correlation_id: CorrelationId,
168 pub principal_id: PrincipalId,
169 pub capabilities: CapabilitySet,
170 pub deadline: DateTime<Utc>,
171 pub idempotency_key: Option<IdempotencyKey>,
172}
173
174impl RequestContext {
175 pub fn new_for_test(
176 principal_id: PrincipalId,
177 capabilities: impl IntoIterator<Item = Capability>,
178 deadline: DateTime<Utc>,
179 ) -> Self {
180 Self {
181 interface_version: InterfaceVersion::CURRENT,
182 correlation_id: CorrelationId::new(),
183 principal_id,
184 capabilities: CapabilitySet::new(capabilities),
185 deadline,
186 idempotency_key: None,
187 }
188 }
189
190 pub fn validate_at(
192 &self,
193 dispatch_time: DateTime<Utc>,
194 ) -> Result<(), InterfaceValidationError> {
195 if self.deadline <= dispatch_time {
196 return Err(InterfaceValidationError::ExpiredDeadline);
197 }
198 Ok(())
199 }
200}
201
202#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
204pub enum InterfaceOperation {
205 RuntimeInfo,
206 CreateSession,
207 ReadSession,
208 DeleteSession,
209 OpenPage,
210 ReadPage,
211 ClosePage,
212 SubmitCommand,
213 CreateCheckpoint,
214 ReadCheckpoint,
215 RecoverWorkflow,
216 ReadArtifact,
217 ReadContext,
218 CaptureArtifact,
219 SubscribeEvents,
220 SubmitJob,
221 ReadJob,
222 CancelJob,
223 IssuePrincipal,
224 RevokePrincipal,
225}
226
227impl InterfaceOperation {
228 pub const fn required(self) -> &'static [Capability] {
229 match self {
230 Self::RuntimeInfo => &[Capability::SessionRead],
231 Self::CreateSession => &[Capability::SessionWrite],
232 Self::ReadSession => &[Capability::SessionRead],
233 Self::DeleteSession => &[Capability::SessionWrite],
234 Self::OpenPage => &[Capability::PageWrite],
235 Self::ReadPage => &[Capability::PageRead],
236 Self::ClosePage => &[Capability::PageWrite],
237 Self::SubmitCommand => &[Capability::BrowserMutate],
238 Self::CreateCheckpoint => &[Capability::RecoveryWrite],
239 Self::ReadCheckpoint => &[Capability::RecoveryRead],
240 Self::RecoverWorkflow => &[Capability::RecoveryWrite],
241 Self::ReadArtifact => &[Capability::ArtifactRead],
242 Self::ReadContext => &[Capability::ContextRead],
243 Self::CaptureArtifact => &[Capability::ArtifactCapture],
244 Self::SubscribeEvents => &[Capability::SessionRead],
245 Self::SubmitJob => &[Capability::JobSubmit],
246 Self::ReadJob => &[Capability::JobRead],
247 Self::CancelJob => &[Capability::JobCancel],
248 Self::IssuePrincipal => &[Capability::AuthorityAdmin],
249 Self::RevokePrincipal => &[Capability::AuthorityAdmin],
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
255#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
256#[serde(tag = "kind", rename_all = "camelCase")]
257pub enum InterfaceEvent {
258 CommandOutcome {
259 cursor: EventCursor,
260 command_id: CommandId,
261 outcome: CommandOutcome,
262 },
263 ArtifactCaptured {
264 cursor: EventCursor,
265 artifact_id: ArtifactId,
266 },
267 EventGap {
268 earliest_available_cursor: EventCursor,
269 },
270}
271
272#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
273#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
274#[serde(rename_all = "camelCase")]
275pub enum InterfaceErrorCode {
276 InvalidRequest,
277 UnsupportedInterfaceVersion,
278 InvalidIdempotencyKey,
279 IdempotencyConflict,
280 DeadlineExceeded,
281 AuthenticationFailed,
282 TokenExpired,
283 MissingCapability,
284 MalformedScope,
285 ArtifactDenied,
286 UnsupportedOperation,
287 NotFound,
288 ResourceExhausted,
289 Internal,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
294#[serde(rename_all = "camelCase")]
295pub struct InterfaceError {
296 pub code: InterfaceErrorCode,
297 pub layer: ErrorLayer,
298 pub message: String,
299 pub correlation_id: CorrelationId,
300 pub command_id: Option<CommandId>,
301 pub retryable: bool,
302 pub retry_after_ms: Option<u64>,
303 pub reconciliation_required: bool,
304 pub required_capability: Option<Capability>,
305}
306
307#[derive(Debug, Clone, Error, Eq, PartialEq)]
308pub enum InterfaceValidationError {
309 #[error("unsupported interface version: {0}")]
310 UnsupportedInterfaceVersion(String),
311 #[error("idempotency key must contain 1-128 printable ASCII characters")]
312 InvalidIdempotencyKey,
313 #[error("request deadline must be after dispatch time")]
314 ExpiredDeadline,
315}