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