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 = 11;
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 ServiceAppFacadeRequest,
173 ScopeOwnedDataPurgeRequested,
174 ArtifactDeliveryProjectionRequested,
175 ArtifactUploadProjectionRequested,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase", deny_unknown_fields)]
180pub struct ExternalArtifactRequestContext {
181 pub tenant_id: String,
182 pub scope_id: String,
183 pub actor_user_id: String,
184 pub client_id: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase", deny_unknown_fields)]
189pub struct ExternalPutArtifactRequest {
190 pub context: ExternalArtifactRequestContext,
191 pub artifact: artifact_api::PutArtifactRequest,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase", deny_unknown_fields)]
196pub struct ExternalOpenArtifactRequest {
197 pub context: ExternalArtifactRequestContext,
198 pub uri: String,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase", deny_unknown_fields)]
203pub struct ExternalAuthorizeArtifactReadRequest {
204 pub grant: String,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct ExternalAuthorizeArtifactUploadRequest {
210 pub grant: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215pub struct ExternalCommitArtifactUploadRequest {
216 pub grant: String,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase", deny_unknown_fields)]
221pub struct ExternalArtifactReadDescriptor {
222 pub path: String,
223 pub kind: artifact_api::ArtifactKind,
224 pub mime_type: String,
225 pub size_bytes: u64,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub width: Option<u32>,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub height: Option<u32>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub duration_millis: Option<u64>,
232 pub sha256: String,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
236#[serde(rename_all = "camelCase", deny_unknown_fields)]
237pub struct ExternalArtifactUploadDescriptor {
238 pub upload_id: String,
239 pub path: String,
240 pub kind: artifact_api::ArtifactKind,
241 pub mime_type: String,
242 pub size_bytes: u64,
243 pub sha256: String,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub duration_millis: Option<u64>,
246 pub expires_at_unix_ms: u64,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase", deny_unknown_fields)]
251pub struct ArtifactDeliveryProjectionRequest {
252 pub grant: String,
253 pub expires_at_unix_ms: u64,
254 pub client_id: String,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258#[serde(rename_all = "camelCase", deny_unknown_fields)]
259pub struct ArtifactDeliveryProjectionResponse {
260 pub url: String,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase", deny_unknown_fields)]
265pub struct ArtifactUploadProjectionRequest {
266 pub grant: String,
267 pub expires_at_unix_ms: u64,
268 pub client_id: String,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "camelCase", deny_unknown_fields)]
273pub struct ArtifactUploadProjectionResponse {
274 pub url: String,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct ExternalServiceAppFacadeRequest {
282 pub target: String,
283 pub tenant_id: String,
284 pub scope_id: String,
285 pub user_id: String,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub client_id: Option<String>,
288 pub payload: Value,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct ExternalHostRuntimeRequest {
299 pub method: ExternalHostRuntimeMethod,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub candidate_id: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub action: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub payload: Option<Value>,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "camelCase")]
310pub enum ExternalHostRuntimeMethod {
311 Discovery,
312 CandidateRequest,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
316#[serde(rename_all = "camelCase")]
317pub struct ScopeOwnedDataPurgeRequest {
318 pub tenant_id: String,
319 pub scope_id: String,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct ExternalCoreServiceEffects {
325 pub effects: Vec<ServiceCoreEffect>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct ExternalCoreEventCompletion {
331 pub event_id: String,
332 pub ok: bool,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub response: Option<Value>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub error: Option<ExternalCoreError>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct HttpPayloadRequest {
341 pub payload: Value,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct EndpointPayloadRequest {
346 pub endpoint_id: String,
347 pub payload: Value,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct PlaygroundPayloadRequest {
352 pub tenant_id: String,
353 pub scope_id: String,
354 pub device_id: String,
355 pub capability_id: String,
356 pub payload: Value,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct ExternalCoreRuntimeMetadata {
362 pub backend: String,
363 pub instance_id: String,
364 pub pid: u32,
365 pub state: String,
366 pub started_at: String,
367 pub protocol_version: u32,
368 pub binary_version: String,
369 pub socket_path: String,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
373#[serde(rename_all = "camelCase")]
374pub struct ExternalCoreHealth {
375 pub ok: bool,
376 pub ready: bool,
377 pub backend: String,
378 pub binary_version: String,
379 pub agent_version: String,
380 pub protocol_version: u32,
381 #[serde(skip_serializing_if = "Option::is_none")]
382 pub instance_id: Option<String>,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub pid: Option<u32>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub state: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub started_at: Option<String>,
389 #[serde(default)]
390 pub capabilities: Vec<String>,
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn external_events_use_the_v11_wire_shape() {
399 let event = ExternalCoreEvent {
400 event_id: "event-1".to_string(),
401 kind: ExternalCoreEventKind::ScopeOwnedDataPurgeRequested,
402 payload: serde_json::to_value(ScopeOwnedDataPurgeRequest {
403 tenant_id: "tenant-1".to_string(),
404 scope_id: "scope-1".to_string(),
405 })
406 .unwrap(),
407 expects_response: true,
408 };
409
410 let value = serde_json::to_value(event).unwrap();
411 assert_eq!(EXTERNAL_CORE_PROTOCOL_VERSION, 11);
412 assert_eq!(value["kind"], "scopeOwnedDataPurgeRequested");
413 assert_eq!(value["payload"]["tenantId"], "tenant-1");
414 assert_eq!(value["payload"]["scopeId"], "scope-1");
415 assert_eq!(value["expectsResponse"], true);
416
417 let facade = ExternalCoreEvent {
418 event_id: "event-2".to_string(),
419 kind: ExternalCoreEventKind::ServiceAppFacadeRequest,
420 payload: serde_json::to_value(ExternalServiceAppFacadeRequest {
421 target: "/app/messaging/provider/list".to_string(),
422 tenant_id: "tenant-1".to_string(),
423 scope_id: "scope-1".to_string(),
424 user_id: "user-1".to_string(),
425 client_id: None,
426 payload: serde_json::json!({"placement": "local"}),
427 })
428 .unwrap(),
429 expects_response: true,
430 };
431 let value = serde_json::to_value(facade).unwrap();
432 assert_eq!(value["kind"], "serviceAppFacadeRequest");
433 assert_eq!(value["payload"]["target"], "/app/messaging/provider/list");
434 assert_eq!(value["payload"]["userId"], "user-1");
435 assert!(value["payload"].get("clientId").is_none());
436 }
437
438 #[test]
439 fn artifact_host_contract_uses_canonical_wire_types() {
440 let descriptor = ExternalArtifactReadDescriptor {
441 path: "/runtime/artifacts/blob".to_string(),
442 kind: artifact_api::ArtifactKind::Audio,
443 mime_type: "audio/ogg".to_string(),
444 size_bytes: 42,
445 width: None,
446 height: None,
447 duration_millis: Some(1_500),
448 sha256: "a".repeat(64),
449 };
450 let value = serde_json::to_value(descriptor).unwrap();
451 assert_eq!(value["kind"], "AUDIO");
452 assert_eq!(value["mimeType"], "audio/ogg");
453 assert_eq!(value["durationMillis"], 1_500);
454 assert!(value.get("width").is_none());
455
456 let request = ArtifactDeliveryProjectionRequest {
457 grant: "payload.signature".to_string(),
458 expires_at_unix_ms: 123,
459 client_id: "L:client".to_string(),
460 };
461 let value = serde_json::to_value(request).unwrap();
462 assert_eq!(value["expiresAtUnixMs"], 123);
463 assert_eq!(value["clientId"], "L:client");
464 }
465}