holochain_conductor_api/
app_interface.rs

1use crate::peer_meta::PeerMetaInfo;
2use crate::{AppAuthenticationToken, ExternalApiWireError};
3use holo_hash::AgentPubKey;
4use holochain_keystore::LairResult;
5use holochain_keystore::MetaLairClient;
6use holochain_types::prelude::*;
7use indexmap::IndexMap;
8use kitsune2_api::Url;
9use std::collections::{BTreeMap, HashMap};
10
11/// Represents the available conductor functions to call over an app interface
12/// and will result in a corresponding [`AppResponse`] message being sent back over the
13/// interface connection.
14///
15/// # Errors
16///
17/// Returns an [`AppResponse::Error`] with a reason why the request failed.
18#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, SerializedBytes)]
19#[serde(tag = "type", content = "value", rename_all = "snake_case")]
20pub enum AppRequest {
21    /// Get info about the app that you are connected to, including info about each cell installed
22    /// by this app.
23    ///
24    /// # Returns
25    ///
26    /// [`AppResponse::AppInfo`]
27    AppInfo,
28
29    /// Request information about the agents in this Conductor's peer store.
30    ///
31    /// This is limited to cells of the app you are connected to.
32    ///
33    /// # Returns
34    ///
35    /// [`AppResponse::AgentInfo`]
36    AgentInfo {
37        /// Optionally limit the results to specific DNA hashes
38        dna_hashes: Option<Vec<DnaHash>>,
39    },
40
41    /// Request the contents of the peer meta store(s) related to
42    /// the given dna hashes for the agent at the given Url.
43    ///
44    /// If `dna_hashes` is set to `None` it returns the contents
45    /// for all dnas of the app.
46    ///
47    /// # Returns
48    ///
49    /// [`AppResponse::PeerMetaInfo`]
50    PeerMetaInfo {
51        url: Url,
52        dna_hashes: Option<Vec<DnaHash>>,
53    },
54
55    /// Call a zome function.
56    ///
57    /// The payload to this call is composed of the serialized [`ZomeCallParams`] as bytes
58    /// and the provenance's signature.
59    ///
60    /// Serialization must be performed with MessagePack. The resulting bytes are hashed using the
61    /// SHA2 512-bit algorithm, and the hash is signed with the provenance's private ed25519 key.
62    /// The hash is not included in the call's payload.
63    ///
64    /// # Returns
65    ///
66    /// [`AppResponse::ZomeCalled`] Indicates the zome call was deserialized successfully. If the
67    /// call was authorized, the response yields the return value of the zome function as MessagePack
68    /// encoded bytes. The bytes can be deserialized to the expected return type.
69    ///
70    /// This response is also returned when authorization of the zome call failed because of an
71    /// invalid signature, capability grant or nonce.
72    ///
73    /// # Errors
74    ///
75    /// [`SerializedBytesError`] is returned when the serialized bytes could not be deserialized
76    /// to the expected [`ZomeCallParams`].
77    CallZome(Box<ZomeCallParamsSigned>),
78
79    /// Get the state of a countersigning session.
80    ///
81    /// # Returns
82    ///
83    /// [`AppResponse::CountersigningSessionState`]
84    ///
85    /// # Errors
86    ///
87    /// [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
88    /// passed in to the call.
89    #[cfg(feature = "unstable-countersigning")]
90    GetCountersigningSessionState(Box<CellId>),
91
92    /// Abandon an unresolved countersigning session.
93    ///
94    /// If the current session has not been resolved automatically, it can be forcefully abandoned.
95    /// A condition for this call to succeed is that at least one attempt has been made to resolve
96    /// it automatically.
97    ///
98    /// # Returns
99    ///
100    /// [`AppResponse::CountersigningSessionAbandoned`]
101    ///
102    /// The session is marked for abandoning and the countersigning workflow was triggered. The session
103    /// has not been abandoned yet.
104    ///
105    /// Upon successful abandoning the system signal [`SystemSignal::AbandonedCountersigning`] will
106    /// be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
107    /// would return `None`.
108    ///
109    /// In the countersigning workflow it will first be attempted to resolve the session with incoming
110    /// signatures of the countersigned entries, before force-abandoning the session. In a very rare event
111    /// it could happen that in just the moment where the [`AppRequest::AbandonCountersigningSession`]
112    /// is made, signatures for this session come in. If they are valid, the session will be resolved and
113    /// published as usual. Should they be invalid, however, the flag to abandon the session is erased.
114    /// In such cases this request can be retried until the session has been abandoned successfully.
115    ///
116    /// # Errors
117    ///
118    /// [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
119    /// passed in to the call.
120    ///
121    /// [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
122    /// cell id.
123    ///
124    /// [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
125    /// automatically has not been made.
126    #[cfg(feature = "unstable-countersigning")]
127    AbandonCountersigningSession(Box<CellId>),
128
129    /// Publish an unresolved countersigning session.
130    ///
131    /// If the current session has not been resolved automatically, it can be forcefully published.
132    /// A condition for this call to succeed is that at least one attempt has been made to resolve
133    /// it automatically.
134    ///
135    /// # Returns
136    ///
137    /// [`AppResponse::PublishCountersigningSessionTriggered`]
138    ///
139    /// The session is marked for publishing and the countersigning workflow was triggered. The session
140    /// has not been published yet.
141    ///
142    /// Upon successful publishing the system signal [`SystemSignal::SuccessfulCountersigning`] will
143    /// be emitted and the session removed from state, so that [`AppRequest::GetCountersigningSessionState`]
144    /// would return `None`.
145    ///
146    /// In the countersigning workflow it will first be attempted to resolve the session with incoming
147    /// signatures of the countersigned entries, before force-publishing the session. In a very rare event
148    /// it could happen that in just the moment where the [`AppRequest::PublishCountersigningSession`]
149    /// is made, signatures for this session come in. If they are valid, the session will be resolved and
150    /// published as usual. Should they be invalid, however, the flag to publish the session is erased.
151    /// In such cases this request can be retried until the session has been published successfully.
152    ///
153    /// # Errors
154    ///
155    /// [`CountersigningError::WorkspaceDoesNotExist`] likely indicates that an invalid cell id was
156    /// passed in to the call.
157    ///
158    /// [`CountersigningError::SessionNotFound`] when no ongoing session could be found for the provided
159    /// cell id.
160    ///
161    /// [`CountersigningError::SessionNotUnresolved`] when an attempt to resolve the session
162    /// automatically has not been made.
163    #[cfg(feature = "unstable-countersigning")]
164    PublishCountersigningSession(Box<CellId>),
165
166    /// Clone a DNA (in the biological sense), thus creating a new `Cell`.
167    ///
168    /// Using the provided, already-registered DNA, create a new DNA with a unique
169    /// ID and the specified properties, create a new cell from this cloned DNA,
170    /// and add the cell to the specified app.
171    ///
172    /// # Returns
173    ///
174    /// [`AppResponse::CloneCellCreated`]
175    CreateCloneCell(Box<CreateCloneCellPayload>),
176
177    /// Disable a clone cell.
178    ///
179    /// Providing a [`CloneId`] or [`CellId`], disable an existing clone cell.
180    /// When the clone cell exists, it is disabled and can not be called any
181    /// longer. If it doesn't exist, the call is a no-op.
182    ///
183    /// # Returns
184    ///
185    /// [`AppResponse::CloneCellDisabled`] if the clone cell existed
186    /// and has been disabled.
187    DisableCloneCell(Box<DisableCloneCellPayload>),
188
189    /// Enable a clone cell that was previously disabled.
190    ///
191    /// # Returns
192    ///
193    /// [`AppResponse::CloneCellEnabled`]
194    EnableCloneCell(Box<EnableCloneCellPayload>),
195
196    /// Retrieve network metrics for the current app.
197    ///
198    /// Identical to what [`AdminRequest::DumpNetworkMetrics`](crate::admin_interface::AdminRequest::DumpNetworkMetrics)
199    /// does, but scoped to the current app.
200    ///
201    /// If `dna_hash` is not set, metrics for all DNAs in the current app are returned.
202    ///
203    /// # Returns
204    ///
205    /// [`AppResponse::NetworkMetricsDumped`]
206    DumpNetworkMetrics {
207        /// If set, limits the metrics dumped to a single DNA hash space.
208        #[serde(default)]
209        dna_hash: Option<DnaHash>,
210
211        /// Whether to include a DHT summary.
212        ///
213        /// You need a dump from multiple nodes in order to make a comparison, so this is not
214        /// requested by default.
215        #[serde(default)]
216        include_dht_summary: bool,
217    },
218
219    /// Dump network statistics from the Kitsune2 networking transport module.
220    ///
221    /// Identical to what [`AdminRequest::DumpNetworkStats`](crate::admin_interface::AdminRequest::DumpNetworkStats)
222    /// does, but scoped to the current app. Connections that are not relevant to a DNA in the
223    /// current app are filtered out.
224    ///
225    /// # Returns
226    ///
227    /// [`AppResponse::NetworkStatsDumped`]
228    DumpNetworkStats,
229
230    /// List all host functions available to wasm on this conductor.
231    ///
232    /// # Returns
233    ///
234    /// [`AppResponse::ListWasmHostFunctions`]
235    ListWasmHostFunctions,
236
237    /// Provide the membrane proofs for this app, if this app was installed
238    /// using `allow_deferred_memproofs` and memproofs were not provided at
239    /// installation time.
240    ///
241    /// # Returns
242    ///
243    /// [`AppResponse::Ok`]
244    ProvideMemproofs(MemproofMap),
245
246    /// Enable the app, only in special circumstances.
247    /// Can only be called while the app is in the `Disabled(NotStartedAfterProvidingMemproofs)` state.
248    /// Cannot be used to enable the app if it's in any other state, or Disabled for any other reason.
249    ///
250    /// # Returns
251    ///
252    /// [`AppResponse::Ok`]
253    EnableApp,
254}
255
256/// Represents the possible responses to an [`AppRequest`].
257#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, SerializedBytes)]
258#[serde(tag = "type", content = "value", rename_all = "snake_case")]
259pub enum AppResponse {
260    /// Can occur in response to any [`AppRequest`].
261    ///
262    /// There has been an error during the handling of the request.
263    Error(ExternalApiWireError),
264
265    /// The successful response to an [`AppRequest::AppInfo`].
266    ///
267    /// Option will be `None` if there is no installed app with the given `installed_app_id`.
268    AppInfo(Option<AppInfo>),
269
270    /// The successful response to an [`AppRequest::AgentInfo`].
271    AgentInfo(Vec<String>),
272
273    /// The successful response to an [`AppRequest::PeerMetaInfo`].
274    ///
275    /// A JSON formatted string.
276    PeerMetaInfo(BTreeMap<DnaHash, BTreeMap<String, PeerMetaInfo>>),
277
278    /// The successful response to an [`AppRequest::CallZome`].
279    ///
280    /// Note that [`ExternIO`] is simply a structure of [`struct@SerializedBytes`], so the client will have
281    /// to decode this response back into the data provided by the zome using a [msgpack] library to utilize it.
282    ///
283    /// [msgpack]: https://msgpack.org/
284    ZomeCalled(Box<ExternIO>),
285
286    /// The successful response to an [`AppRequest::GetCountersigningSessionState`].
287    #[cfg(feature = "unstable-countersigning")]
288    CountersigningSessionState(Box<Option<CountersigningSessionState>>),
289
290    /// The successful response to an [`AppRequest::AbandonCountersigningSession`].
291    #[cfg(feature = "unstable-countersigning")]
292    CountersigningSessionAbandoned,
293
294    /// The successful response to an [`AppRequest::PublishCountersigningSession`].
295    #[cfg(feature = "unstable-countersigning")]
296    PublishCountersigningSessionTriggered,
297
298    /// The successful response to an [`AppRequest::CreateCloneCell`].
299    ///
300    /// The response contains the created clone [`ClonedCell`].
301    CloneCellCreated(ClonedCell),
302
303    /// The successful response to an [`AppRequest::DisableCloneCell`].
304    ///
305    /// An existing clone cell has been disabled.
306    CloneCellDisabled,
307
308    /// The successful response to an [`AppRequest::EnableCloneCell`].
309    ///
310    /// A previously disabled clone cell has been enabled. The [`ClonedCell`]
311    /// is returned.
312    CloneCellEnabled(ClonedCell),
313
314    /// The successful result of a call to [`AppRequest::DumpNetworkMetrics`].
315    NetworkMetricsDumped(HashMap<DnaHash, Kitsune2NetworkMetrics>),
316
317    /// The successful result of a call to [`AppRequest::DumpNetworkStats`].
318    NetworkStatsDumped(kitsune2_api::TransportStats),
319
320    /// All the wasm host functions supported by this conductor.
321    ListWasmHostFunctions(Vec<String>),
322
323    /// Operation successful, no payload.
324    Ok,
325}
326
327/// The data provided over an app interface in order to make a zome call.
328#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
329pub struct ZomeCallParamsSigned {
330    /// Bytes of the serialized zome call payload that consists of all fields of the
331    /// [`ZomeCallParams`].
332    pub bytes: ExternIO,
333    /// Signature by the provenance of the call, signing the bytes of the zome call payload.
334    pub signature: Signature,
335}
336
337impl ZomeCallParamsSigned {
338    pub fn new(bytes: Vec<u8>, signature: Signature) -> Self {
339        Self {
340            bytes: ExternIO::from(bytes),
341            signature,
342        }
343    }
344
345    pub async fn try_from_params(
346        keystore: &MetaLairClient,
347        params: ZomeCallParams,
348    ) -> LairResult<Self> {
349        let (bytes, bytes_hash) = params.serialize_and_hash().map_err(|e| e.to_string())?;
350        let signature = params
351            .provenance
352            .sign_raw(keystore, bytes_hash.into())
353            .await?;
354        Ok(Self::new(bytes, signature))
355    }
356}
357
358#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
359#[serde(tag = "type", content = "value", rename_all = "snake_case")]
360pub enum CellInfo {
361    /// Cells provisioned at app installation as defined in the bundle.
362    Provisioned(ProvisionedCell),
363
364    // Cells created at runtime by cloning provisioned cells.
365    Cloned(ClonedCell),
366
367    /// Potential cells with deferred installation as defined in the bundle.
368    /// Not yet implemented.
369    Stem(StemCell),
370}
371
372impl CellInfo {
373    pub fn new_provisioned(cell_id: CellId, dna_modifiers: DnaModifiers, name: String) -> Self {
374        Self::Provisioned(ProvisionedCell {
375            cell_id,
376            dna_modifiers,
377            name,
378        })
379    }
380
381    pub fn new_cloned(
382        cell_id: CellId,
383        clone_id: CloneId,
384        original_dna_hash: DnaHash,
385        dna_modifiers: DnaModifiers,
386        name: String,
387        enabled: bool,
388    ) -> Self {
389        Self::Cloned(ClonedCell {
390            cell_id,
391            clone_id,
392            original_dna_hash,
393            dna_modifiers,
394            name,
395            enabled,
396        })
397    }
398}
399
400/// Cell whose instantiation has been deferred.
401/// Not yet implemented.
402#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
403pub struct StemCell {
404    /// The hash of the DNA that this cell would be instantiated from
405    pub original_dna_hash: DnaHash,
406    /// The DNA modifiers that will be used when instantiating the cell
407    pub dna_modifiers: DnaModifiers,
408    /// An optional name to override the cell's bundle name when instantiating
409    pub name: Option<String>,
410}
411
412/// Provisioned cell, a cell instantiated from a DNA on app installation.
413#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
414pub struct ProvisionedCell {
415    /// The cell's identifying data
416    pub cell_id: CellId,
417    /// The DNA modifiers that were used to instantiate the cell
418    pub dna_modifiers: DnaModifiers,
419    /// The name the cell was instantiated with
420    pub name: String,
421}
422
423/// Info about an installed app, returned as part of [`AppResponse::AppInfo`]
424#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, SerializedBytes)]
425pub struct AppInfo {
426    /// The unique identifier for an installed app in this conductor
427    pub installed_app_id: InstalledAppId,
428    /// Info about the cells installed in this app. Lists of cells are ordered
429    /// and contain first the provisioned cell, then enabled clone cells and
430    /// finally disabled clone cells.
431    pub cell_info: IndexMap<RoleName, Vec<CellInfo>>,
432    /// The app's current status, in an API-friendly format
433    pub status: AppStatus,
434    /// The app's agent pub key.
435    pub agent_pub_key: AgentPubKey,
436    /// The original AppManifest used to install the app, which can also be used to
437    /// install the app again under a new agent.
438    pub manifest: AppManifest,
439    /// The timestamp when this app was installed.
440    pub installed_at: Timestamp,
441}
442
443impl AppInfo {
444    pub fn from_installed_app(
445        app: &InstalledApp,
446        dna_definitions: &IndexMap<CellId, DnaDefHashed>,
447    ) -> Self {
448        let installed_app_id = app.id().clone();
449        let status = app.status().clone();
450        let agent_pub_key = app.agent_key().to_owned();
451        let mut manifest = app.manifest().clone();
452        let installed_at = *app.installed_at();
453
454        let mut cell_info: IndexMap<RoleName, Vec<CellInfo>> = IndexMap::new();
455        app.roles().iter().for_each(|(role_name, role_assignment)| {
456            // create a vector with info of all cells for this role
457            let mut cell_info_for_role: Vec<CellInfo> = Vec::new();
458
459            // push the base cell to the vector of cell infos
460            if let Some(provisioned_dna_hash) = role_assignment.provisioned_dna_hash() {
461                let provisioned_cell_id =
462                    CellId::new(provisioned_dna_hash.clone(), agent_pub_key.clone());
463                if let Some(dna_def) = dna_definitions.get(&provisioned_cell_id) {
464                    // TODO: populate `enabled` with cell state once it is implemented for a base cell
465                    let cell_info = CellInfo::new_provisioned(
466                        provisioned_cell_id.clone(),
467                        dna_def.modifiers.to_owned(),
468                        dna_def.name.to_owned(),
469                    );
470                    cell_info_for_role.push(cell_info);
471
472                    // Update the manifest with the installed hash
473                    match &mut manifest {
474                        AppManifest::V0(manifest) => {
475                            if let Some(role) =
476                                manifest.roles.iter_mut().find(|r| r.name == *role_name)
477                            {
478                                role.dna.installed_hash = Some(dna_def.hash.clone().into());
479                            }
480                        }
481                    }
482                } else {
483                    tracing::error!(
484                        "no DNA definition found for cell id {}",
485                        provisioned_cell_id
486                    );
487                }
488            } else {
489                // no provisioned cell, thus there must be a deferred cell
490                // this is not implemented as of now
491                unimplemented!()
492            };
493
494            // push enabled clone cells to the vector of cell infos
495            if let Some(clone_cells) = app.clone_cells_for_role_name(role_name) {
496                clone_cells.for_each(|(clone_id, cell_id)| {
497                    if let Some(dna_def) = dna_definitions.get(&cell_id) {
498                        let cell_info = CellInfo::new_cloned(
499                            cell_id,
500                            clone_id.to_owned(),
501                            dna_def.hash.to_owned(),
502                            dna_def.modifiers.to_owned(),
503                            dna_def.name.to_owned(),
504                            true,
505                        );
506                        cell_info_for_role.push(cell_info);
507                    } else {
508                        tracing::error!("no DNA definition found for cell id {}", cell_id);
509                    }
510                });
511            }
512
513            // push disabled clone cells to the vector of cell infos
514            if let Some(clone_cells) = app.disabled_clone_cells_for_role_name(role_name) {
515                clone_cells.for_each(|(clone_id, cell_id)| {
516                    if let Some(dna_def) = dna_definitions.get(&cell_id) {
517                        let cell_info = CellInfo::new_cloned(
518                            cell_id,
519                            clone_id.to_owned(),
520                            dna_def.hash.to_owned(),
521                            dna_def.modifiers.to_owned(),
522                            dna_def.name.to_owned(),
523                            false,
524                        );
525                        cell_info_for_role.push(cell_info);
526                    } else {
527                        tracing::error!("no DNA definition found for cell id {}", cell_id);
528                    }
529                });
530            }
531
532            cell_info.insert(role_name.clone(), cell_info_for_role);
533        });
534
535        Self {
536            installed_app_id,
537            cell_info,
538            status,
539            agent_pub_key,
540            manifest,
541            installed_at,
542        }
543    }
544}
545
546/// The request payload sent on a Holochain app websocket to authenticate the connection.
547#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, SerializedBytes)]
548pub struct AppAuthenticationRequest {
549    /// The authentication token that was provided by the conductor when [`crate::admin_interface::AdminRequest::IssueAppAuthenticationToken`] was called.
550    pub token: AppAuthenticationToken,
551}
552
553#[cfg(test)]
554mod tests {
555    use crate::{AppRequest, AppResponse};
556    use holochain_types::app::{AppStatus, DisabledAppReason};
557    use serde::Deserialize;
558
559    #[test]
560    fn app_request_serialization() {
561        use rmp_serde::Deserializer;
562
563        // make sure requests are serialized as expected
564        let request = AppRequest::AppInfo;
565        let serialized_request = holochain_serialized_bytes::encode(&request).unwrap();
566        assert_eq!(
567            serialized_request,
568            vec![129, 164, 116, 121, 112, 101, 168, 97, 112, 112, 95, 105, 110, 102, 111]
569        );
570
571        let json_expected = r#"{"type":"app_info"}"#;
572        let mut deserializer = Deserializer::new(&*serialized_request);
573        let json_value: serde_json::Value = Deserialize::deserialize(&mut deserializer).unwrap();
574        let json_actual = serde_json::to_string(&json_value).unwrap();
575
576        assert_eq!(json_actual, json_expected);
577
578        // make sure responses are serialized as expected
579        let response = AppResponse::ListWasmHostFunctions(vec![
580            "host_fn_1".to_string(),
581            "host_fn_2".to_string(),
582        ]);
583        let serialized_response = holochain_serialized_bytes::encode(&response).unwrap();
584        assert_eq!(
585            serialized_response,
586            vec![
587                130, 164, 116, 121, 112, 101, 184, 108, 105, 115, 116, 95, 119, 97, 115, 109, 95,
588                104, 111, 115, 116, 95, 102, 117, 110, 99, 116, 105, 111, 110, 115, 165, 118, 97,
589                108, 117, 101, 146, 169, 104, 111, 115, 116, 95, 102, 110, 95, 49, 169, 104, 111,
590                115, 116, 95, 102, 110, 95, 50
591            ]
592        );
593
594        let json_expected =
595            r#"{"type":"list_wasm_host_functions","value":["host_fn_1","host_fn_2"]}"#;
596        let mut deserializer = Deserializer::new(&*serialized_response);
597        let json_value: serde_json::Value = Deserialize::deserialize(&mut deserializer).unwrap();
598        let json_actual = serde_json::to_string(&json_value).unwrap();
599
600        assert_eq!(json_actual, json_expected);
601    }
602
603    #[test]
604    fn status_serialization() {
605        use serde_json;
606
607        let status = AppStatus::Disabled(DisabledAppReason::Error("because".into()));
608
609        assert_eq!(
610            serde_json::to_string(&status).unwrap(),
611            "{\"type\":\"disabled\",\"value\":{\"type\":\"error\",\"value\":\"because\"}}"
612        );
613
614        let status = AppStatus::Disabled(DisabledAppReason::User);
615
616        assert_eq!(
617            serde_json::to_string(&status).unwrap(),
618            "{\"type\":\"disabled\",\"value\":{\"type\":\"user\"}}",
619        );
620    }
621}