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