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 = 10;
10pub const ARTIFACT_CONTENT_PATH_PREFIX: &str = "/artifact/v1/content/";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ExternalCoreRequest {
14 pub id: String,
15 pub method: ExternalCoreMethod,
16 pub payload: Value,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ExternalCoreResponse {
21 pub id: String,
22 pub ok: bool,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub result: Option<Value>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub error: Option<ExternalCoreError>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ExternalCoreError {
31 pub code: String,
32 pub message: String,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub enum ExternalCoreMethod {
37 HandleCoreInput,
38 RegisterLocalAppClient,
39 RegisterNodeConnection,
40 CleanupWsState,
41 UpdateCallbackBase,
42 PutArtifact,
43 OpenArtifact,
44 AuthorizeArtifactRead,
45 DeviceIdentity,
46 CommissionFingerprint,
47 CommissionPublicKeyBase64,
48 ListMdnsRecords,
49 PollEvents,
50 CompleteEvents,
51 CommissionChallengePayload,
52 PairingStartPayload,
53 SetupStartPayload,
54 GeneralWebhookPayload,
55 PlaygroundWebhookPayload,
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 ServiceEffects,
168 HostRuntimeRequest,
169 ServiceAppFacadeRequest,
170 ScopeOwnedDataPurgeRequested,
171 ArtifactDeliveryProjectionRequested,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176pub struct ExternalArtifactRequestContext {
177 pub tenant_id: String,
178 pub scope_id: String,
179 pub actor_user_id: String,
180 pub client_id: String,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct ExternalPutArtifactRequest {
186 pub context: ExternalArtifactRequestContext,
187 pub artifact: artifact_api::PutArtifactRequest,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(rename_all = "camelCase", deny_unknown_fields)]
192pub struct ExternalOpenArtifactRequest {
193 pub context: ExternalArtifactRequestContext,
194 pub uri: String,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase", deny_unknown_fields)]
199pub struct ExternalAuthorizeArtifactReadRequest {
200 pub grant: String,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase", deny_unknown_fields)]
205pub struct ExternalArtifactReadDescriptor {
206 pub path: String,
207 pub kind: artifact_api::ArtifactKind,
208 pub mime_type: String,
209 pub size_bytes: u64,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub width: Option<u32>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub height: Option<u32>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub duration_millis: Option<u64>,
216 pub sha256: String,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase", deny_unknown_fields)]
221pub struct ArtifactDeliveryProjectionRequest {
222 pub grant: String,
223 pub expires_at_unix_ms: u64,
224 pub client_id: String,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase", deny_unknown_fields)]
229pub struct ArtifactDeliveryProjectionResponse {
230 pub url: String,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct ExternalServiceAppFacadeRequest {
238 pub target: String,
239 pub tenant_id: String,
240 pub scope_id: String,
241 pub user_id: String,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub client_id: Option<String>,
244 pub payload: Value,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct ExternalHostRuntimeRequest {
255 pub method: ExternalHostRuntimeMethod,
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub candidate_id: Option<String>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub action: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub payload: Option<Value>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
265#[serde(rename_all = "camelCase")]
266pub enum ExternalHostRuntimeMethod {
267 Discovery,
268 CandidateRequest,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "camelCase")]
273pub struct ScopeOwnedDataPurgeRequest {
274 pub tenant_id: String,
275 pub scope_id: String,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279#[serde(rename_all = "camelCase")]
280pub struct ExternalCoreServiceEffects {
281 pub effects: Vec<ServiceCoreEffect>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ExternalCoreEventCompletion {
287 pub event_id: String,
288 pub ok: bool,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub response: Option<Value>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub error: Option<ExternalCoreError>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct HttpPayloadRequest {
297 pub payload: Value,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct EndpointPayloadRequest {
302 pub endpoint_id: String,
303 pub payload: Value,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct PlaygroundPayloadRequest {
308 pub tenant_id: String,
309 pub scope_id: String,
310 pub device_id: String,
311 pub capability_id: String,
312 pub payload: Value,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct ExternalCoreRuntimeMetadata {
318 pub backend: String,
319 pub instance_id: String,
320 pub pid: u32,
321 pub state: String,
322 pub started_at: String,
323 pub protocol_version: u32,
324 pub binary_version: String,
325 pub socket_path: String,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct ExternalCoreHealth {
331 pub ok: bool,
332 pub ready: bool,
333 pub backend: String,
334 pub binary_version: String,
335 pub agent_version: String,
336 pub protocol_version: u32,
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub instance_id: Option<String>,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub pid: Option<u32>,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub state: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub started_at: Option<String>,
345 #[serde(default)]
346 pub capabilities: Vec<String>,
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn external_events_use_the_v10_wire_shape() {
355 let event = ExternalCoreEvent {
356 event_id: "event-1".to_string(),
357 kind: ExternalCoreEventKind::ScopeOwnedDataPurgeRequested,
358 payload: serde_json::to_value(ScopeOwnedDataPurgeRequest {
359 tenant_id: "tenant-1".to_string(),
360 scope_id: "scope-1".to_string(),
361 })
362 .unwrap(),
363 expects_response: true,
364 };
365
366 let value = serde_json::to_value(event).unwrap();
367 assert_eq!(EXTERNAL_CORE_PROTOCOL_VERSION, 10);
368 assert_eq!(value["kind"], "scopeOwnedDataPurgeRequested");
369 assert_eq!(value["payload"]["tenantId"], "tenant-1");
370 assert_eq!(value["payload"]["scopeId"], "scope-1");
371 assert_eq!(value["expectsResponse"], true);
372
373 let facade = ExternalCoreEvent {
374 event_id: "event-2".to_string(),
375 kind: ExternalCoreEventKind::ServiceAppFacadeRequest,
376 payload: serde_json::to_value(ExternalServiceAppFacadeRequest {
377 target: "/app/messaging/provider/list".to_string(),
378 tenant_id: "tenant-1".to_string(),
379 scope_id: "scope-1".to_string(),
380 user_id: "user-1".to_string(),
381 client_id: None,
382 payload: serde_json::json!({"placement": "local"}),
383 })
384 .unwrap(),
385 expects_response: true,
386 };
387 let value = serde_json::to_value(facade).unwrap();
388 assert_eq!(value["kind"], "serviceAppFacadeRequest");
389 assert_eq!(value["payload"]["target"], "/app/messaging/provider/list");
390 assert_eq!(value["payload"]["userId"], "user-1");
391 assert!(value["payload"].get("clientId").is_none());
392 }
393
394 #[test]
395 fn artifact_host_contract_uses_canonical_wire_types() {
396 let descriptor = ExternalArtifactReadDescriptor {
397 path: "/runtime/artifacts/blob".to_string(),
398 kind: artifact_api::ArtifactKind::Audio,
399 mime_type: "audio/ogg".to_string(),
400 size_bytes: 42,
401 width: None,
402 height: None,
403 duration_millis: Some(1_500),
404 sha256: "a".repeat(64),
405 };
406 let value = serde_json::to_value(descriptor).unwrap();
407 assert_eq!(value["kind"], "AUDIO");
408 assert_eq!(value["mimeType"], "audio/ogg");
409 assert_eq!(value["durationMillis"], 1_500);
410 assert!(value.get("width").is_none());
411
412 let request = ArtifactDeliveryProjectionRequest {
413 grant: "payload.signature".to_string(),
414 expires_at_unix_ms: 123,
415 client_id: "L:client".to_string(),
416 };
417 let value = serde_json::to_value(request).unwrap();
418 assert_eq!(value["expiresAtUnixMs"], 123);
419 assert_eq!(value["clientId"], "L:client");
420 }
421}