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