Skip to main content

core_api/
external_rpc.rs

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/// An App Facade request delegated by an externally hosted Core to the
278/// application service that owns the target's business logic.
279#[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/// A platform capability request emitted by an externally hosted Core.
292///
293/// Core owns the onboarding workflow. The native host owns LAN discovery and
294/// transport to a discovered candidate, so Android can satisfy this contract
295/// without moving application logic into the mobile shell.
296#[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}