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}