1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::{AuthRequest, AuthenticatedSession, ServiceCoreInput, ServiceCoreOutput};
5use std::collections::HashMap;
6
7pub const EXTERNAL_CORE_PROTOCOL_VERSION: u32 = 13;
8pub const ARTIFACT_CONTENT_PATH_PREFIX: &str = "/artifact/v1/content/";
9pub const ARTIFACT_UPLOAD_PATH_PREFIX: &str = "/artifact/v1/upload/";
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ExternalCoreRequest {
13 pub id: String,
14 pub method: ExternalCoreMethod,
15 pub payload: Value,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ExternalCoreResponse {
20 pub id: String,
21 pub ok: bool,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub result: Option<Value>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub error: Option<ExternalCoreError>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ExternalCoreError {
30 pub code: String,
31 pub message: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum ExternalCoreMethod {
36 HandleCoreInput,
37 RegisterLocalAppClient,
38 RegisterNodeConnection,
39 CleanupWsState,
40 UpdateCallbackBase,
41 PutArtifact,
42 OpenArtifact,
43 AuthorizeArtifactRead,
44 AuthorizeArtifactUpload,
45 CommitArtifactUpload,
46 DeviceIdentity,
47 CommissionFingerprint,
48 CommissionPublicKeyBase64,
49 ListMdnsRecords,
50 PollEvents,
51 CompleteEvents,
52 CommissionChallengePayload,
53 PairingStartPayload,
54 SetupStartPayload,
55 GeneralWebhookPayload,
56 PlaygroundWebhookPayload,
57 InvokeWasmPayload,
58 Health,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct HandleCoreInputRequest {
63 pub input: ServiceCoreInput,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct HandleCoreInputResponse {
68 pub output: ServiceCoreOutput,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RegisterLocalAppClientRequest {
73 pub ws_id: String,
74 pub auth_request: AuthRequest,
75 pub session: AuthenticatedSession,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct RegisterNodeConnectionRequest {
80 pub ws_id: String,
81 pub session_id: String,
82 pub node_type: String,
83 pub node_id: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub host_name: Option<String>,
86 pub tenant_id: String,
87 pub scope_id: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct CleanupWsStateRequest {
92 pub ws_id: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct UpdateCallbackBaseRequest {
97 pub callback_base: String,
98 pub host: String,
99 pub port: u16,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ExternalCoreDeviceIdentity {
105 pub device_id: String,
106 pub device_name: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct ListMdnsRecordsRequest {
112 pub port: u16,
113 pub addresses: Vec<String>,
114 pub host_type: String,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct ExternalCoreMdnsRecord {
120 pub service_type: String,
121 pub instance_name: String,
122 pub port: u16,
123 pub properties: HashMap<String, String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct PollExternalCoreEventsRequest {
129 pub consumer_id: String,
130 pub max_events: u16,
131 pub timeout_ms: u64,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135#[serde(rename_all = "camelCase")]
136pub struct PollExternalCoreEventsResponse {
137 pub events: Vec<ExternalCoreEvent>,
138 pub timed_out: bool,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct CompleteExternalCoreEventsRequest {
144 pub consumer_id: String,
145 pub completions: Vec<ExternalCoreEventCompletion>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct CompleteExternalCoreEventsResponse {
151 pub completed_event_ids: Vec<String>,
152 pub missing_event_ids: Vec<String>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct ExternalCoreEvent {
158 pub event_id: String,
159 pub kind: ExternalCoreEventKind,
160 pub payload: Value,
161 pub expects_response: bool,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "camelCase")]
166pub enum ExternalCoreEventKind {
167 MdnsRecordsChanged,
168 DeliveryRequested,
169 ConnectionControlRequested,
170 HostRuntimeRequest,
171 ServiceAppFacadeRequest,
172 ScopeOwnedDataPurgeRequested,
173 ArtifactDeliveryProjectionRequested,
174 ArtifactUploadProjectionRequested,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase", deny_unknown_fields)]
181pub struct MessagingDeliveryRequest {
182 pub tenant_id: String,
183 pub scope_id: String,
184 pub surface_id: String,
185 pub target: String,
186 pub payload: String,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase", deny_unknown_fields)]
191pub struct MessagingDeliveryResponse {
192 pub delivered: bool,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase", deny_unknown_fields)]
197pub struct ExternalArtifactRequestContext {
198 pub tenant_id: String,
199 pub scope_id: String,
200 pub actor_user_id: String,
201 pub client_id: String,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase", deny_unknown_fields)]
206pub struct ExternalPutArtifactRequest {
207 pub context: ExternalArtifactRequestContext,
208 pub artifact: artifact_api::PutArtifactRequest,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase", deny_unknown_fields)]
213pub struct ExternalOpenArtifactRequest {
214 pub context: ExternalArtifactRequestContext,
215 pub uri: String,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(rename_all = "camelCase", deny_unknown_fields)]
220pub struct ExternalAuthorizeArtifactReadRequest {
221 pub grant: String,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase", deny_unknown_fields)]
226pub struct ExternalAuthorizeArtifactUploadRequest {
227 pub grant: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase", deny_unknown_fields)]
232pub struct ExternalCommitArtifactUploadRequest {
233 pub grant: String,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase", deny_unknown_fields)]
238pub struct ExternalArtifactReadDescriptor {
239 pub path: String,
240 pub kind: artifact_api::ArtifactKind,
241 pub mime_type: String,
242 pub size_bytes: u64,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub width: Option<u32>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub height: Option<u32>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub duration_millis: Option<u64>,
249 pub sha256: String,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase", deny_unknown_fields)]
254pub struct ExternalArtifactUploadDescriptor {
255 pub upload_id: String,
256 pub path: String,
257 pub kind: artifact_api::ArtifactKind,
258 pub mime_type: String,
259 pub size_bytes: u64,
260 pub sha256: String,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub duration_millis: Option<u64>,
263 pub expires_at_unix_ms: u64,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase", deny_unknown_fields)]
268pub struct ArtifactDeliveryProjectionRequest {
269 pub grant: String,
270 pub expires_at_unix_ms: u64,
271 pub client_id: String,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "camelCase", deny_unknown_fields)]
276pub struct ArtifactDeliveryProjectionResponse {
277 pub url: String,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "camelCase", deny_unknown_fields)]
282pub struct ArtifactUploadProjectionRequest {
283 pub grant: String,
284 pub expires_at_unix_ms: u64,
285 pub client_id: String,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "camelCase", deny_unknown_fields)]
290pub struct ArtifactUploadProjectionResponse {
291 pub url: String,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct ExternalServiceAppFacadeRequest {
299 pub target: String,
300 pub tenant_id: String,
301 pub scope_id: String,
302 pub user_id: String,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub client_id: Option<String>,
305 pub payload: Value,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase")]
315pub struct ExternalHostRuntimeRequest {
316 pub method: ExternalHostRuntimeMethod,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub candidate_id: Option<String>,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub action: Option<String>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub payload: Option<Value>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
326#[serde(rename_all = "camelCase")]
327pub enum ExternalHostRuntimeMethod {
328 Discovery,
329 CandidateRequest,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
333#[serde(rename_all = "camelCase")]
334pub struct ScopeOwnedDataPurgeRequest {
335 pub tenant_id: String,
336 pub scope_id: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340#[serde(rename_all = "camelCase")]
341pub struct ExternalCoreEventCompletion {
342 pub event_id: String,
343 pub ok: bool,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 pub response: Option<Value>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub error: Option<ExternalCoreError>,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct HttpPayloadRequest {
352 pub payload: Value,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct EndpointPayloadRequest {
357 pub endpoint_id: String,
358 pub payload: Value,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct PlaygroundPayloadRequest {
363 pub tenant_id: String,
364 pub scope_id: String,
365 pub device_id: String,
366 pub capability_id: String,
367 pub payload: Value,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct ExternalCoreRuntimeMetadata {
373 pub backend: String,
374 pub instance_id: String,
375 pub pid: u32,
376 pub state: String,
377 pub started_at: String,
378 pub protocol_version: u32,
379 pub binary_version: String,
380 pub socket_path: String,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct ExternalCoreHealth {
386 pub ok: bool,
387 pub ready: bool,
388 pub backend: String,
389 pub binary_version: String,
390 pub agent_version: String,
391 pub protocol_version: u32,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub instance_id: Option<String>,
394 #[serde(skip_serializing_if = "Option::is_none")]
395 pub pid: Option<u32>,
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub state: Option<String>,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub started_at: Option<String>,
400 #[serde(default)]
401 pub capabilities: Vec<String>,
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn messaging_delivery_is_a_closed_request_response_contract() {
410 let request = serde_json::json!({"tenantId":"t", "scopeId":"s", "surfaceId":"m",
411 "target":"/chat/event", "payload":"{}"});
412 let decoded: MessagingDeliveryRequest = serde_json::from_value(request.clone()).unwrap();
413 assert_eq!(serde_json::to_value(decoded).unwrap(), request);
414 let mut invalid = request;
415 invalid["unknown"] = serde_json::json!(true);
416 assert!(serde_json::from_value::<MessagingDeliveryRequest>(invalid).is_err());
417 assert!(
418 serde_json::from_value::<MessagingDeliveryResponse>(serde_json::json!({})).is_err()
419 );
420 assert_eq!(
421 serde_json::to_value(ExternalCoreEventKind::DeliveryRequested).unwrap(),
422 "deliveryRequested"
423 );
424 }
425
426 #[test]
427 fn external_events_use_the_v12_wire_shape() {
428 let event = ExternalCoreEvent {
429 event_id: "event-1".to_string(),
430 kind: ExternalCoreEventKind::ScopeOwnedDataPurgeRequested,
431 payload: serde_json::to_value(ScopeOwnedDataPurgeRequest {
432 tenant_id: "tenant-1".to_string(),
433 scope_id: "scope-1".to_string(),
434 })
435 .unwrap(),
436 expects_response: true,
437 };
438
439 let value = serde_json::to_value(event).unwrap();
440 assert_eq!(EXTERNAL_CORE_PROTOCOL_VERSION, 13);
441 assert_eq!(value["kind"], "scopeOwnedDataPurgeRequested");
442 assert_eq!(value["payload"]["tenantId"], "tenant-1");
443 assert_eq!(value["payload"]["scopeId"], "scope-1");
444 assert_eq!(value["expectsResponse"], true);
445
446 let facade = ExternalCoreEvent {
447 event_id: "event-2".to_string(),
448 kind: ExternalCoreEventKind::ServiceAppFacadeRequest,
449 payload: serde_json::to_value(ExternalServiceAppFacadeRequest {
450 target: "/app/messaging/provider/list".to_string(),
451 tenant_id: "tenant-1".to_string(),
452 scope_id: "scope-1".to_string(),
453 user_id: "user-1".to_string(),
454 client_id: None,
455 payload: serde_json::json!({"placement": "local"}),
456 })
457 .unwrap(),
458 expects_response: true,
459 };
460 let value = serde_json::to_value(facade).unwrap();
461 assert_eq!(value["kind"], "serviceAppFacadeRequest");
462 assert_eq!(value["payload"]["target"], "/app/messaging/provider/list");
463 assert_eq!(value["payload"]["userId"], "user-1");
464 assert!(value["payload"].get("clientId").is_none());
465 }
466
467 #[test]
468 fn artifact_host_contract_uses_canonical_wire_types() {
469 let descriptor = ExternalArtifactReadDescriptor {
470 path: "/runtime/artifacts/blob".to_string(),
471 kind: artifact_api::ArtifactKind::Audio,
472 mime_type: "audio/ogg".to_string(),
473 size_bytes: 42,
474 width: None,
475 height: None,
476 duration_millis: Some(1_500),
477 sha256: "a".repeat(64),
478 };
479 let value = serde_json::to_value(descriptor).unwrap();
480 assert_eq!(value["kind"], "AUDIO");
481 assert_eq!(value["mimeType"], "audio/ogg");
482 assert_eq!(value["durationMillis"], 1_500);
483 assert!(value.get("width").is_none());
484
485 let request = ArtifactDeliveryProjectionRequest {
486 grant: "payload.signature".to_string(),
487 expires_at_unix_ms: 123,
488 client_id: "L:client".to_string(),
489 };
490 let value = serde_json::to_value(request).unwrap();
491 assert_eq!(value["expiresAtUnixMs"], 123);
492 assert_eq!(value["clientId"], "L:client");
493 }
494}