Skip to main content

core_api/
external_rpc.rs

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