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 = 12;
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    MessagingDeliveryRequested,
173    ServiceAppFacadeRequest,
174    ScopeOwnedDataPurgeRequested,
175    ArtifactDeliveryProjectionRequested,
176    ArtifactUploadProjectionRequested,
177}
178
179/// Messaging-only Core -> Host request. A successful response means delivery was accepted by
180/// the provider implementation, not merely queued in the Core event pump or read by a user.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase", deny_unknown_fields)]
183pub struct MessagingDeliveryRequest {
184    pub tenant_id: String,
185    pub scope_id: String,
186    pub surface_id: String,
187    pub target: String,
188    pub payload: String,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase", deny_unknown_fields)]
193pub struct MessagingDeliveryResponse {
194    pub delivered: bool,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase", deny_unknown_fields)]
199pub struct ExternalArtifactRequestContext {
200    pub tenant_id: String,
201    pub scope_id: String,
202    pub actor_user_id: String,
203    pub client_id: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase", deny_unknown_fields)]
208pub struct ExternalPutArtifactRequest {
209    pub context: ExternalArtifactRequestContext,
210    pub artifact: artifact_api::PutArtifactRequest,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215pub struct ExternalOpenArtifactRequest {
216    pub context: ExternalArtifactRequestContext,
217    pub uri: String,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[serde(rename_all = "camelCase", deny_unknown_fields)]
222pub struct ExternalAuthorizeArtifactReadRequest {
223    pub grant: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase", deny_unknown_fields)]
228pub struct ExternalAuthorizeArtifactUploadRequest {
229    pub grant: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase", deny_unknown_fields)]
234pub struct ExternalCommitArtifactUploadRequest {
235    pub grant: String,
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239#[serde(rename_all = "camelCase", deny_unknown_fields)]
240pub struct ExternalArtifactReadDescriptor {
241    pub path: String,
242    pub kind: artifact_api::ArtifactKind,
243    pub mime_type: String,
244    pub size_bytes: u64,
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub width: Option<u32>,
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub height: Option<u32>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub duration_millis: Option<u64>,
251    pub sha256: String,
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "camelCase", deny_unknown_fields)]
256pub struct ExternalArtifactUploadDescriptor {
257    pub upload_id: String,
258    pub path: String,
259    pub kind: artifact_api::ArtifactKind,
260    pub mime_type: String,
261    pub size_bytes: u64,
262    pub sha256: String,
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub duration_millis: Option<u64>,
265    pub expires_at_unix_ms: u64,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[serde(rename_all = "camelCase", deny_unknown_fields)]
270pub struct ArtifactDeliveryProjectionRequest {
271    pub grant: String,
272    pub expires_at_unix_ms: u64,
273    pub client_id: String,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase", deny_unknown_fields)]
278pub struct ArtifactDeliveryProjectionResponse {
279    pub url: String,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
283#[serde(rename_all = "camelCase", deny_unknown_fields)]
284pub struct ArtifactUploadProjectionRequest {
285    pub grant: String,
286    pub expires_at_unix_ms: u64,
287    pub client_id: String,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase", deny_unknown_fields)]
292pub struct ArtifactUploadProjectionResponse {
293    pub url: String,
294}
295
296/// An App Facade request delegated by an externally hosted Core to the
297/// application service that owns the target's business logic.
298#[derive(Debug, Clone, Serialize, Deserialize)]
299#[serde(rename_all = "camelCase")]
300pub struct ExternalServiceAppFacadeRequest {
301    pub target: String,
302    pub tenant_id: String,
303    pub scope_id: String,
304    pub user_id: String,
305    #[serde(default, skip_serializing_if = "Option::is_none")]
306    pub client_id: Option<String>,
307    pub payload: Value,
308}
309
310/// A platform capability request emitted by an externally hosted Core.
311///
312/// Core owns the onboarding workflow. The native host owns LAN discovery and
313/// transport to a discovered candidate, so Android can satisfy this contract
314/// without moving application logic into the mobile shell.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct ExternalHostRuntimeRequest {
318    pub method: ExternalHostRuntimeMethod,
319    #[serde(default, skip_serializing_if = "Option::is_none")]
320    pub candidate_id: Option<String>,
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub action: Option<String>,
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub payload: Option<Value>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
328#[serde(rename_all = "camelCase")]
329pub enum ExternalHostRuntimeMethod {
330    Discovery,
331    CandidateRequest,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335#[serde(rename_all = "camelCase")]
336pub struct ScopeOwnedDataPurgeRequest {
337    pub tenant_id: String,
338    pub scope_id: String,
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct ExternalCoreServiceEffects {
344    pub effects: Vec<ServiceCoreEffect>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct ExternalCoreEventCompletion {
350    pub event_id: String,
351    pub ok: bool,
352    #[serde(default, skip_serializing_if = "Option::is_none")]
353    pub response: Option<Value>,
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub error: Option<ExternalCoreError>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct HttpPayloadRequest {
360    pub payload: Value,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct EndpointPayloadRequest {
365    pub endpoint_id: String,
366    pub payload: Value,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PlaygroundPayloadRequest {
371    pub tenant_id: String,
372    pub scope_id: String,
373    pub device_id: String,
374    pub capability_id: String,
375    pub payload: Value,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct ExternalCoreRuntimeMetadata {
381    pub backend: String,
382    pub instance_id: String,
383    pub pid: u32,
384    pub state: String,
385    pub started_at: String,
386    pub protocol_version: u32,
387    pub binary_version: String,
388    pub socket_path: String,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(rename_all = "camelCase")]
393pub struct ExternalCoreHealth {
394    pub ok: bool,
395    pub ready: bool,
396    pub backend: String,
397    pub binary_version: String,
398    pub agent_version: String,
399    pub protocol_version: u32,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub instance_id: Option<String>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub pid: Option<u32>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub state: Option<String>,
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub started_at: Option<String>,
408    #[serde(default)]
409    pub capabilities: Vec<String>,
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    #[test]
417    fn messaging_delivery_is_a_closed_request_response_contract() {
418        let request = serde_json::json!({"tenantId":"t", "scopeId":"s", "surfaceId":"m",
419            "target":"/chat/event", "payload":"{}"});
420        let decoded: MessagingDeliveryRequest = serde_json::from_value(request.clone()).unwrap();
421        assert_eq!(serde_json::to_value(decoded).unwrap(), request);
422        let mut invalid = request;
423        invalid["unknown"] = serde_json::json!(true);
424        assert!(serde_json::from_value::<MessagingDeliveryRequest>(invalid).is_err());
425        assert!(
426            serde_json::from_value::<MessagingDeliveryResponse>(serde_json::json!({})).is_err()
427        );
428        assert_eq!(
429            serde_json::to_value(ExternalCoreEventKind::MessagingDeliveryRequested).unwrap(),
430            "messagingDeliveryRequested"
431        );
432    }
433
434    #[test]
435    fn external_events_use_the_v12_wire_shape() {
436        let event = ExternalCoreEvent {
437            event_id: "event-1".to_string(),
438            kind: ExternalCoreEventKind::ScopeOwnedDataPurgeRequested,
439            payload: serde_json::to_value(ScopeOwnedDataPurgeRequest {
440                tenant_id: "tenant-1".to_string(),
441                scope_id: "scope-1".to_string(),
442            })
443            .unwrap(),
444            expects_response: true,
445        };
446
447        let value = serde_json::to_value(event).unwrap();
448        assert_eq!(EXTERNAL_CORE_PROTOCOL_VERSION, 12);
449        assert_eq!(value["kind"], "scopeOwnedDataPurgeRequested");
450        assert_eq!(value["payload"]["tenantId"], "tenant-1");
451        assert_eq!(value["payload"]["scopeId"], "scope-1");
452        assert_eq!(value["expectsResponse"], true);
453
454        let facade = ExternalCoreEvent {
455            event_id: "event-2".to_string(),
456            kind: ExternalCoreEventKind::ServiceAppFacadeRequest,
457            payload: serde_json::to_value(ExternalServiceAppFacadeRequest {
458                target: "/app/messaging/provider/list".to_string(),
459                tenant_id: "tenant-1".to_string(),
460                scope_id: "scope-1".to_string(),
461                user_id: "user-1".to_string(),
462                client_id: None,
463                payload: serde_json::json!({"placement": "local"}),
464            })
465            .unwrap(),
466            expects_response: true,
467        };
468        let value = serde_json::to_value(facade).unwrap();
469        assert_eq!(value["kind"], "serviceAppFacadeRequest");
470        assert_eq!(value["payload"]["target"], "/app/messaging/provider/list");
471        assert_eq!(value["payload"]["userId"], "user-1");
472        assert!(value["payload"].get("clientId").is_none());
473    }
474
475    #[test]
476    fn artifact_host_contract_uses_canonical_wire_types() {
477        let descriptor = ExternalArtifactReadDescriptor {
478            path: "/runtime/artifacts/blob".to_string(),
479            kind: artifact_api::ArtifactKind::Audio,
480            mime_type: "audio/ogg".to_string(),
481            size_bytes: 42,
482            width: None,
483            height: None,
484            duration_millis: Some(1_500),
485            sha256: "a".repeat(64),
486        };
487        let value = serde_json::to_value(descriptor).unwrap();
488        assert_eq!(value["kind"], "AUDIO");
489        assert_eq!(value["mimeType"], "audio/ogg");
490        assert_eq!(value["durationMillis"], 1_500);
491        assert!(value.get("width").is_none());
492
493        let request = ArtifactDeliveryProjectionRequest {
494            grant: "payload.signature".to_string(),
495            expires_at_unix_ms: 123,
496            client_id: "L:client".to_string(),
497        };
498        let value = serde_json::to_value(request).unwrap();
499        assert_eq!(value["expiresAtUnixMs"], 123);
500        assert_eq!(value["clientId"], "L:client");
501    }
502}