use anyhow::anyhow;
use chrono::{DateTime, Utc};
use clap::{Args, Parser, Subcommand};
use holo_hash::{ActionHash, AgentPubKeyB64, DnaHashB64};
use holochain_client::AdminWebsocket;
use holochain_conductor_api::AppStatusFilter;
use holochain_conductor_api::InterfaceDriver;
use holochain_conductor_api::PeerMetaInfo;
use holochain_conductor_api::{AdminInterfaceConfig, AppInfo};
use holochain_types::app::AppManifest;
use holochain_types::app::RoleSettingsMap;
use holochain_types::app::RoleSettingsMapYaml;
use holochain_types::prelude::NetworkSeed;
use holochain_types::prelude::{AgentPubKey, AppBundleSource};
use holochain_types::prelude::{CellId, InstallAppPayload};
use holochain_types::prelude::{Deserialize, Serialize};
use holochain_types::prelude::{DnaHash, InstalledAppId};
use holochain_types::websocket::AllowedOrigins;
use kitsune2_api::AgentInfoSigned;
use kitsune2_api::Url;
use kitsune2_core::Ed25519Verifier;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::path::PathBuf;
use std::sync::Arc;
#[doc(hidden)]
#[derive(Debug, Parser)]
pub struct Call {
#[arg(short, long)]
pub port: u16,
#[arg(long)]
pub origin: Option<String>,
#[command(subcommand)]
pub call: AdminRequestCli,
}
#[derive(Debug, Subcommand, Clone)]
pub enum AdminRequestCli {
AddAdminWs(AddAdminWs),
AddAppWs(AddAppWs),
InstallApp(InstallApp),
UninstallApp(UninstallApp),
ListAppWs,
ListDnas,
NewAgent,
ListCells,
ListApps(ListApps),
EnableApp(EnableApp),
DisableApp(DisableApp),
DumpState(DumpState),
DumpConductorState,
DumpNetworkMetrics(DumpNetworkMetrics),
DumpNetworkStats,
ListCapabilityGrants(ListCapGrants),
RevokeZomeCallCapability(RevokeZomeCallCapability),
AddAgents(AgentInfos),
ListAgents(ListAgents),
PeerMetaInfo(PeerMetaInfoArgs),
}
#[derive(Debug, Args, Clone)]
pub struct AddAdminWs {
pub port: Option<u16>,
#[arg(long)]
pub danger_bind_addr: Option<String>,
#[arg(long, default_value_t = AllowedOrigins::Any)]
pub allowed_origins: AllowedOrigins,
}
#[derive(Debug, Args, Clone)]
pub struct AddAppWs {
pub port: Option<u16>,
#[arg(long)]
pub danger_bind_addr: Option<String>,
#[arg(long, default_value_t = AllowedOrigins::Any)]
pub allowed_origins: AllowedOrigins,
#[arg(long)]
pub installed_app_id: Option<InstalledAppId>,
}
#[derive(Debug, Args, Clone)]
pub struct InstallApp {
#[arg(long)]
pub app_id: Option<String>,
#[arg(long, value_parser = parse_agent_key)]
pub agent_key: Option<AgentPubKey>,
#[arg(required = true)]
pub path: PathBuf,
pub network_seed: Option<NetworkSeed>,
pub roles_settings: Option<PathBuf>,
}
#[derive(Debug, Args, Clone)]
pub struct UninstallApp {
pub app_id: String,
#[arg(long, default_value_t = false)]
pub force: bool,
}
#[derive(Debug, Args, Clone)]
pub struct EnableApp {
pub app_id: String,
}
#[derive(Debug, Args, Clone)]
pub struct DisableApp {
pub app_id: String,
}
#[derive(Debug, Args, Clone)]
pub struct DumpState {
#[arg(value_parser = parse_dna_hash)]
pub dna: DnaHash,
#[arg(value_parser = parse_agent_key)]
pub agent_key: AgentPubKey,
}
#[derive(Debug, Args, Clone)]
pub struct DumpNetworkMetrics {
#[arg(value_parser = parse_dna_hash)]
pub dna: Option<DnaHash>,
#[arg(long)]
pub include_dht_summary: bool,
}
#[derive(Debug, Args, Clone)]
pub struct ListCapGrants {
pub installed_app_id: String,
pub include_revoked: bool,
}
#[derive(Debug, Args, Clone)]
pub struct RevokeZomeCallCapability {
pub action_hash: String,
pub dna_hash: String,
pub agent_key: String,
}
#[derive(Debug, Args, Clone)]
pub struct AgentInfos {
pub agent_infos: String,
}
#[derive(Debug, Args, Clone)]
pub struct ListAgents {
#[arg(short, long, num_args = 0.., value_parser = parse_dna_hash)]
pub dna: Option<Vec<DnaHash>>,
}
#[derive(Debug, Args, Clone)]
pub struct ListApps {
#[arg(short, long, value_parser = parse_status_filter)]
pub status: Option<AppStatusFilter>,
}
#[derive(Debug, Args, Clone)]
pub struct PeerMetaInfoArgs {
#[arg(long)]
pub url: Url,
#[arg(short, long, num_args = 0.., value_parser = parse_dna_hash)]
pub dna: Option<Vec<DnaHash>>,
}
#[doc(hidden)]
pub async fn call(req: Call) -> anyhow::Result<()> {
let Call { port, origin, call } = req;
let mut client = AdminWebsocket::connect(format!("localhost:{port}"), origin.clone()).await?;
call_inner(&mut client, call).await?;
Ok(())
}
async fn call_inner(client: &mut AdminWebsocket, call: AdminRequestCli) -> anyhow::Result<()> {
match call {
AdminRequestCli::AddAdminWs(args) => {
let port = args.port.unwrap_or(0);
client
.add_admin_interfaces(vec![AdminInterfaceConfig {
driver: InterfaceDriver::Websocket {
port,
danger_bind_addr: args.danger_bind_addr,
allowed_origins: args.allowed_origins,
},
}])
.await?;
crate::msg!("Added admin port {}", port);
}
AdminRequestCli::AddAppWs(args) => {
let port = args.port.unwrap_or(0);
let port = client
.attach_app_interface(
port,
args.danger_bind_addr,
args.allowed_origins,
args.installed_app_id,
)
.await?;
crate::msg!("Added app port {}", port);
}
AdminRequestCli::ListAppWs => {
let interface_infos = client.list_app_interfaces().await?;
println!("{}", serde_json::to_string(&interface_infos)?);
}
AdminRequestCli::InstallApp(args) => {
let app = install_app_bundle(client, args).await?;
println!("{}", app_info_to_base64_json(app)?);
}
AdminRequestCli::UninstallApp(args) => {
client
.uninstall_app(args.app_id.clone(), args.force)
.await?;
crate::msg!("Uninstalled app: \"{}\"", args.app_id);
}
AdminRequestCli::ListDnas => {
let dnas: Vec<DnaHashB64> = client
.list_dnas()
.await?
.into_iter()
.map(|d| d.into())
.collect();
println!("{}", serde_json::to_string(&dnas)?);
}
AdminRequestCli::NewAgent => {
let agent = client.generate_agent_pub_key().await?;
println!("{}", serde_json::to_string(&agent.to_string())?);
}
AdminRequestCli::ListCells => {
let cell_id_jsons: Vec<serde_json::Value> = client
.list_cell_ids()
.await?
.iter()
.map(|id| cell_id_json_to_base64_json(serde_json::to_value(id)?))
.collect::<Result<Vec<serde_json::Value>, serde_json::Error>>()?;
println!("{}", serde_json::to_string(&cell_id_jsons)?);
}
AdminRequestCli::ListApps(args) => {
let apps = client.list_apps(args.status).await?;
let values = apps
.into_iter()
.map(app_info_to_base64_json)
.collect::<Result<Vec<serde_json::Value>, serde_json::Error>>()?;
println!("{}", serde_json::to_string(&values)?);
}
AdminRequestCli::EnableApp(args) => {
client.enable_app(args.app_id.clone()).await?;
crate::msg!("Enabled app: \"{}\"", args.app_id);
}
AdminRequestCli::DisableApp(args) => {
client.disable_app(args.app_id.clone()).await?;
crate::msg!("Disabled app: \"{}\"", args.app_id);
}
AdminRequestCli::DumpState(args) => {
let state = client.dump_state(args.into()).await?;
println!("{state}");
}
AdminRequestCli::DumpConductorState => {
let state = client.dump_conductor_state().await?;
println!("{state}");
}
AdminRequestCli::DumpNetworkMetrics(args) => {
let metrics = client
.dump_network_metrics(args.dna, args.include_dht_summary)
.await?;
println!(
"{}",
serde_json::to_string(
&metrics
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect::<HashMap<_, _>>()
)?
);
}
AdminRequestCli::DumpNetworkStats => {
let stats = client.dump_network_stats().await?;
println!("{}", serde_json::to_string(&stats)?);
}
AdminRequestCli::RevokeZomeCallCapability(args) => {
let action_hash = ActionHash::try_from(&args.action_hash)
.map_err(|e| anyhow!("Invalid action hash: {e}"))?;
let dna_hash =
DnaHash::try_from(&args.dna_hash).map_err(|e| anyhow!("Invalid DNA hash: {e}"))?;
let agent_key = AgentPubKey::try_from(&args.agent_key)
.map_err(|e| anyhow!("Invalid agent key: {e}"))?;
let cell_id = CellId::new(dna_hash, agent_key);
client
.revoke_zome_call_capability(cell_id, action_hash)
.await?;
crate::msg!(
"Revoked zome call capability for action hash: {}",
args.action_hash
);
}
AdminRequestCli::ListCapabilityGrants(args) => {
let info = client
.list_capability_grants(args.installed_app_id, args.include_revoked)
.await?;
println!("{}", serde_json::to_string(&info)?);
}
AdminRequestCli::AddAgents(args) => {
let agent_infos_results =
AgentInfoSigned::decode_list(&Ed25519Verifier, args.agent_infos.as_bytes())?;
let agent_infos = agent_infos_results
.into_iter()
.map(|r| r.expect("Failed to decode agent info."))
.collect();
add_agent_info(client, agent_infos).await?;
}
AdminRequestCli::ListAgents(args) => {
let mut out = Vec::new();
let agent_infos = request_agent_info(client, args).await?;
let cell_info = client.list_cell_ids().await?;
let agents = cell_info
.iter()
.map(|c| c.agent_pubkey().clone())
.map(|a| (a.clone(), a.to_k2_agent()))
.collect::<Vec<_>>();
let dnas = cell_info
.iter()
.map(|c| c.dna_hash().clone())
.map(|d| (d.clone(), d.to_k2_space()))
.collect::<Vec<_>>();
for info in agent_infos {
let this_agent = agents.iter().find(|a| info.agent == a.1);
let this_dna = dnas.iter().find(|d| info.space == d.1).unwrap();
let dt = DateTime::from_timestamp_micros(info.created_at.as_micros())
.ok_or_else(|| anyhow!("Agent info created_at timestamp out of range"))?;
let exp = DateTime::from_timestamp_micros(info.expires_at.as_micros())
.ok_or_else(|| anyhow!("Agent info expires_at timestamp out of range"))?;
out.push(AgentResponse {
agent_pub_key: this_agent.map(|a| a.0.clone().into()),
k2_agent: this_agent.map(|a| a.1.clone()),
dna_hash: this_dna.0.clone().into(),
k2_space: this_dna.1.clone(),
signed_at: dt,
expires_at: exp,
url: info.url.clone(),
});
}
println!("{}", serde_json::to_string(&out)?);
}
AdminRequestCli::PeerMetaInfo(args) => {
let info = client.peer_meta_info(args.url, args.dna).await?;
let string_key_info = info
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect::<BTreeMap<String, BTreeMap<String, PeerMetaInfo>>>();
println!("{}", serde_json::to_string(&string_key_info)?);
}
}
Ok(())
}
fn app_info_to_base64_json(app_info: AppInfo) -> Result<serde_json::Value, serde_json::Error> {
let value = serde_json::to_value(&app_info)?;
let serde_json::Value::Object(mut app_info_map) = value else {
return Err(serde::de::Error::custom(
"Invalid appInfo conversion result",
));
};
if let Some(old_value) = app_info_map.get("agent_pub_key") {
let new_value =
AgentPubKey::from_raw_39(value_to_base64(old_value.to_owned())?).to_string();
app_info_map.insert(
"agent_pub_key".to_string(),
serde_json::Value::String(new_value),
);
}
if let Some(old_value) = app_info_map.get("cell_info") {
let new_value = cell_id_to_base64_within_cell_info_map(old_value.clone())?;
app_info_map.insert("cell_info".to_string(), new_value);
}
Ok(serde_json::Value::Object(app_info_map))
}
fn cell_id_to_base64_within_cell_info_map(
value: serde_json::Value,
) -> Result<serde_json::Value, serde_json::Error> {
let serde_json::Value::Object(mut cell_info_map_map) = value else {
return Err(serde::de::Error::custom("Value is not a CellInfo map."));
};
for (key, val) in cell_info_map_map.iter_mut() {
let serde_json::Value::Array(arr) = val else {
return Err(serde::de::Error::custom(format!(
"Value for `{key}` is not an array."
)));
};
for item in arr.iter_mut() {
let serde_json::Value::Object(ref mut cell_info_map) = item else {
return Err(serde::de::Error::custom("Value is not a CellInfo value."));
};
if let Some(old_map) = cell_info_map.get_mut("value") {
let serde_json::Value::Object(ref mut cell_id_map) = old_map else {
return Err(serde::de::Error::custom("Value is not a CellInfo."));
};
if let Some(old_value) = cell_id_map.get("cell_id") {
let new_value = cell_id_json_to_base64_json(old_value.to_owned())?;
cell_id_map.insert("cell_id".to_string(), new_value);
}
}
}
}
Ok(serde_json::Value::Object(cell_info_map_map))
}
#[derive(Serialize, Deserialize, Debug)]
struct CellIdJson {
pub dna_hash: String,
pub agent_pub_key: String,
}
fn cell_id_json_to_base64_json(
value: serde_json::Value,
) -> Result<serde_json::Value, serde_json::Error> {
let serde_json::Value::Array(arr) = value else {
return Err(serde::de::Error::custom("Value is not an array."));
};
if arr.len() != 2 {
return Err(serde::de::Error::custom(
"Value of type array does not have a length of 2.",
));
};
let cell_id_json = CellIdJson {
dna_hash: DnaHash::from_raw_39(value_to_base64(arr[0].clone())?).to_string(),
agent_pub_key: AgentPubKey::from_raw_39(value_to_base64(arr[1].clone())?).to_string(),
};
serde_json::to_value(cell_id_json)
}
fn value_to_base64(value: serde_json::Value) -> Result<Vec<u8>, serde_json::Error> {
let serde_json::Value::Array(arr) = value else {
return Err(serde::de::Error::custom("Value is not an array."));
};
let mut bytes = Vec::new();
for item in arr {
let serde_json::Value::Number(num) = item else {
return Err(serde::de::Error::custom("Value is not a number."));
};
let Some(n) = num.as_i64() else {
return Err(serde::de::Error::custom("Value is not an integer."));
};
if !(0..=255).contains(&n) {
return Err(serde::de::Error::custom("Value is not an u8."));
}
bytes.push(n as u8);
}
Ok(bytes)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct AgentResponse {
agent_pub_key: Option<AgentPubKeyB64>,
k2_agent: Option<kitsune2_api::AgentId>,
dna_hash: DnaHashB64,
k2_space: kitsune2_api::SpaceId,
signed_at: DateTime<Utc>,
expires_at: DateTime<Utc>,
url: Option<Url>,
}
pub async fn install_app_bundle(
client: &mut AdminWebsocket,
args: InstallApp,
) -> anyhow::Result<AppInfo> {
let InstallApp {
app_id,
agent_key,
path,
network_seed,
roles_settings,
} = args;
let roles_settings = match roles_settings {
Some(path) => {
let yaml_string = std::fs::read_to_string(path)?;
let roles_settings_yaml = serde_yaml::from_str::<RoleSettingsMapYaml>(&yaml_string)?;
let mut roles_settings: RoleSettingsMap = HashMap::new();
for (k, v) in roles_settings_yaml.into_iter() {
roles_settings.insert(k, v.into());
}
Some(roles_settings)
}
None => None,
};
let payload = InstallAppPayload {
installed_app_id: app_id.clone(),
agent_key,
source: AppBundleSource::Path(path),
roles_settings,
network_seed,
ignore_genesis_failure: false,
};
let installed_app = client.install_app(payload).await?;
match &installed_app.manifest {
AppManifest::V0(manifest) => {
if !manifest.allow_deferred_memproofs {
client
.enable_app(installed_app.installed_app_id.clone())
.await?;
}
}
}
Ok(installed_app)
}
pub async fn add_agent_info(
client: &mut AdminWebsocket,
args: Vec<Arc<AgentInfoSigned>>,
) -> anyhow::Result<()> {
let mut agent_infos = Vec::new();
for info in args {
agent_infos.push(info.encode()?);
}
Ok(client.add_agent_info(agent_infos).await?)
}
async fn request_agent_info(
client: &mut AdminWebsocket,
args: ListAgents,
) -> anyhow::Result<Vec<Arc<AgentInfoSigned>>> {
let resp = client.agent_info(args.dna).await?;
let mut out = Vec::new();
for info in resp {
out.push(AgentInfoSigned::decode(
&kitsune2_core::Ed25519Verifier,
info.as_bytes(),
)?);
}
Ok(out)
}
fn parse_agent_key(arg: &str) -> anyhow::Result<AgentPubKey> {
AgentPubKey::try_from(arg).map_err(|e| anyhow::anyhow!("{e:?}"))
}
fn parse_dna_hash(arg: &str) -> anyhow::Result<DnaHash> {
DnaHash::try_from(arg).map_err(|e| anyhow::anyhow!("{e:?}"))
}
fn parse_status_filter(arg: &str) -> anyhow::Result<AppStatusFilter> {
match arg {
"active" => Ok(AppStatusFilter::Enabled),
"inactive" => Ok(AppStatusFilter::Disabled),
_ => Err(anyhow::anyhow!(
"Bad app status filter value: {arg}, only 'active' and 'inactive' are possible"
)),
}
}
impl From<CellId> for DumpState {
fn from(cell_id: CellId) -> Self {
let (dna, agent_key) = cell_id.into_dna_and_agent();
Self { dna, agent_key }
}
}
impl From<DumpState> for CellId {
fn from(ds: DumpState) -> Self {
CellId::new(ds.dna, ds.agent_key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use holo_hash::{AgentPubKey, DnaHash};
use holochain_client::{SerializedBytes, Timestamp};
use holochain_conductor_api::CellInfo;
use holochain_types::app::{AppManifestV0Builder, AppRoleManifest, AppStatus};
use holochain_types::prelude::{CellId, DnaModifiers, RoleName};
use indexmap::IndexMap;
fn test_dna_hash(bytes: u8) -> DnaHash {
DnaHash::from_raw_36(vec![bytes; 36])
}
fn test_agent_key(bytes: u8) -> AgentPubKey {
AgentPubKey::from_raw_36(vec![bytes; 36])
}
fn test_cell_id(dna: u8, agent: u8) -> CellId {
CellId::new(test_dna_hash(dna), test_agent_key(agent))
}
#[test]
fn valid_cell_id_json_to_base64_json() {
let cell_id = test_cell_id(0, 1);
let cell_id_json = serde_json::to_value(&cell_id).unwrap();
let cell_id_base64_json = cell_id_json_to_base64_json(cell_id_json).unwrap();
let cell_id_struct: CellIdJson = serde_json::from_value(cell_id_base64_json).unwrap();
let _struct_json = serde_json::to_value(&cell_id_struct).unwrap();
assert_eq!(cell_id.dna_hash().to_string(), cell_id_struct.dna_hash);
assert_eq!(
cell_id.agent_pubkey().to_string(),
cell_id_struct.agent_pub_key
);
}
#[test]
fn invalid_cell_id_json_to_base64_json() {
let bad_cell_id = vec!["1", "2"];
let cell_id_json = serde_json::to_value(&bad_cell_id).unwrap();
let cell_id_base64_json = cell_id_json_to_base64_json(cell_id_json);
assert!(cell_id_base64_json.is_err());
}
#[test]
fn app_info_to_json() {
let cell_info = CellInfo::new_provisioned(
test_cell_id(2, 3),
DnaModifiers {
network_seed: "sample-seed".to_string(),
properties: SerializedBytes::default(),
},
"sample-info".to_string(),
);
let mut cell_info_map: IndexMap<RoleName, Vec<CellInfo>> = IndexMap::new();
cell_info_map.insert("sample-role".to_string(), vec![cell_info]);
let role_manifest = AppRoleManifest::sample("sample-dna".to_string());
let sample_app_manifest_v0 = AppManifestV0Builder::default()
.name("sample-app".to_string())
.description(Some("Some description".to_string()))
.roles(vec![role_manifest.clone()])
.build()
.unwrap();
let sample_app_manifest = AppManifest::V0(sample_app_manifest_v0.clone());
let app_info = AppInfo {
installed_app_id: "test-app".to_string(),
status: AppStatus::Enabled,
agent_pub_key: test_agent_key(4),
installed_at: Timestamp(42),
manifest: sample_app_manifest,
cell_info: cell_info_map,
};
let app_info_json = app_info_to_base64_json(app_info.clone()).unwrap();
let app_info_2 = serde_json::from_value::<AppInfo>(app_info_json.clone());
assert!(app_info_2.is_err());
let serde_json::Value::Object(app_info_map) = app_info_json else {
panic!("Invalid appInfo conversion result");
};
let Some(agent_value) = app_info_map.get("agent_pub_key") else {
panic!("Invalid appInfo conversion result");
};
let agent_value_str = serde_json::from_value::<String>(agent_value.to_owned()).unwrap();
assert_eq!(app_info.agent_pub_key.to_string(), agent_value_str);
}
#[test]
fn invalid_cell_info_map_json() {
let bad_cell_id = vec!["1", "2"];
let cell_id_json = serde_json::to_value(&bad_cell_id).unwrap();
let cell_id_base64_json = cell_id_to_base64_within_cell_info_map(cell_id_json);
assert!(cell_id_base64_json.is_err());
}
#[test]
fn valid_cell_info_map_json() {
let cell_id_1 = test_cell_id(5, 6);
let cell_info = CellInfo::new_provisioned(
cell_id_1.clone(),
DnaModifiers {
network_seed: "sample-seed".to_string(),
properties: SerializedBytes::default(),
},
"sample-info".to_string(),
);
let mut cell_info_map: IndexMap<RoleName, Vec<CellInfo>> = IndexMap::new();
cell_info_map.insert("role1".to_string(), vec![cell_info.clone()]);
cell_info_map.insert(
"role2".to_string(),
vec![cell_info.clone(), cell_info.clone()],
);
let cell_info_map_json = serde_json::to_value(&cell_info_map).unwrap();
let conv = cell_id_to_base64_within_cell_info_map(cell_info_map_json.clone()).unwrap();
let cell_info_map_2 =
serde_json::from_value::<IndexMap<RoleName, Vec<CellInfo>>>(conv.clone());
assert!(cell_info_map_2.is_err());
let serde_json::Value::Object(cell_info_map_map) = conv else {
panic!("Invalid cell_info map conversion result");
};
let Some(cells_value) = cell_info_map_map.get("role2") else {
panic!("Invalid cell_info map conversion result");
};
let serde_json::Value::Array(cell_info_vec) = cells_value else {
panic!("Invalid cell_info map conversion result");
};
let serde_json::Value::Object(ref cell_info_2) = cell_info_vec[1] else {
panic!("Invalid cell_info map conversion result");
};
let Some(cell_info_obj) = cell_info_2.get("value") else {
panic!("Invalid cell_info map conversion result");
};
let serde_json::Value::Object(ref cell_info_value) = cell_info_obj else {
panic!("Invalid cell_info map conversion result");
};
let Some(cell_id_value) = cell_info_value.get("cell_id") else {
panic!("Invalid cell_info map conversion result");
};
let serde_json::Value::Object(ref cell_id_obj) = cell_id_value else {
panic!("Invalid cell_info map conversion result");
};
let Some(dna_value) = cell_id_obj.get("dna_hash") else {
panic!("Invalid cell_info map conversion result");
};
let dna_b64 = serde_json::from_value::<String>(dna_value.to_owned()).unwrap();
assert_eq!(cell_id_1.dna_hash().to_string(), dna_b64);
let Some(agent_value) = cell_id_obj.get("agent_pub_key") else {
panic!("Invalid cell_info map conversion result");
};
let agent_b64 = serde_json::from_value::<String>(agent_value.to_owned()).unwrap();
assert_eq!(cell_id_1.agent_pubkey().to_string(), agent_b64);
}
}