A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Added DID auth check on incoming msgs, unique topic every session + other fixes

- We now verify the DID coming from the msg can be derived from the sender's Public key
- Instead of hardcoded topic for every session, from now it will be unique topics (online mode only)
- refactor
- Build changes for 0.4.5

madclaws 9682b0ff 69d99535

+174 -132
+1 -1
Cargo.lock
··· 6084 6084 6085 6085 [[package]] 6086 6086 name = "tiles" 6087 - version = "0.4.4" 6087 + version = "0.4.5" 6088 6088 dependencies = [ 6089 6089 "anyhow", 6090 6090 "async-std",
+7 -7
scripts/bundler.sh
··· 22 22 cp "target/${TARGET}/${BINARY_NAME}" "${DIST_DIR}/tmp/" 23 23 24 24 # flushing this folder, else the final zip will have previous app-server zips too (#84) 25 - rm -rf "${SERVER_DIR}/stack_export_prod" 25 + # rm -rf "${SERVER_DIR}/stack_export_prod" 26 26 27 - echo "🔒 Locking the venvstack...." 27 + # echo "🔒 Locking the venvstack...." 28 28 29 - venvstacks lock server/stack/venvstacks.toml 29 + # venvstacks lock server/stack/venvstacks.toml 30 30 31 - echo "🛠️ Building the venvstack...." 31 + # echo "🛠️ Building the venvstack...." 32 32 33 - venvstacks build server/stack/venvstacks.toml 33 + # venvstacks build server/stack/venvstacks.toml 34 34 35 - echo "📦 Publishing the venvstack...." 35 + # echo "📦 Publishing the venvstack...." 36 36 37 - venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml 37 + # venvstacks publish --tag-outputs --output-dir ../stack_export_prod server/stack/venvstacks.toml 38 38 39 39 cp -r "${SERVER_DIR}" "${DIST_DIR}/tmp/" 40 40
+1 -1
scripts/install.sh
··· 5 5 REPO="tilesprivacy/tiles" 6 6 # VERSION=$(grep '^version' tiles/Cargo.toml | head -1 | awk -F'"' '{print $2}') 7 7 8 - VERSION="0.4.4" 8 + VERSION="0.4.5" 9 9 INSTALL_DIR="/usr/local/bin" # CLI install location 10 10 11 11 SERVER_DIR="/usr/local/share/tiles/server" # Python server folder
+1 -1
server/stack/requirements/app-server/packages-app-server.txt
··· 23 23 packaging==26.0 24 24 pathspec==1.0.4 25 25 platformdirs==4.9.4 26 - protobuf==7.34.0 26 + protobuf==7.34.1 27 27 pydantic==2.12.5 28 28 pydantic-core==2.41.5 29 29 pytokens==0.4.1
+2 -2
server/stack/requirements/app-server/pylock.app-server.meta.json
··· 1 1 { 2 2 "lock_input_hash": "sha256:c836d5cfb697330a57241b2b8f275a804178488ec906b19866809ef33c95ba81", 3 3 "lock_version": 1, 4 - "locked_at": "2026-03-15T22:15:15.536434+00:00", 4 + "locked_at": "2026-03-22T21:45:51.270370+00:00", 5 5 "other_inputs_hash": "sha256:63b3c2cfe2ec414938e81dace7aac779c7b902bae681618cd8827e9f16880985", 6 - "requirements_hash": "sha256:71fb833c54864760da900c69c2a0829e19fef2c6b6e8c174162fdb7f021a4eb3", 6 + "requirements_hash": "sha256:167a9044a762af6b1b0b26589b77a245bf351a3b74860bc9f1bc6a07053a48a7", 7 7 "version_inputs_hash": "sha256:58db986b7cd72eeded675f7c9afd8138fe024fb51451131b5562922bbde3cf43" 8 8 }
+13 -13
server/stack/requirements/app-server/pylock.app-server.toml
··· 508 508 509 509 [[packages]] 510 510 name = "protobuf" 511 - version = "7.34.0" 511 + version = "7.34.1" 512 512 index = "https://pypi.org/simple" 513 513 514 514 [[packages.wheels]] 515 - url = "https://files.pythonhosted.org/packages/13/c4/6322ab5c8f279c4c358bc14eb8aefc0550b97222a39f04eb3c1af7a830fa/protobuf-7.34.0-cp310-abi3-macosx_10_9_universal2.whl" 516 - upload-time = 2026-02-27T00:30:14Z 517 - size = 429248 515 + url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl" 516 + upload-time = 2026-03-20T17:34:37Z 517 + size = 429247 518 518 519 519 [packages.wheels.hashes] 520 - sha256 = "8e329966799f2c271d5e05e236459fe1cbfdb8755aaa3b0914fa60947ddea408" 520 + sha256 = "d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7" 521 521 522 522 [[packages.wheels]] 523 - url = "https://files.pythonhosted.org/packages/b5/57/89727baef7578897af5ed166735ceb315819f1c184da8c3441271dbcfde7/protobuf-7.34.0-cp310-abi3-manylinux2014_x86_64.whl" 524 - upload-time = 2026-02-27T00:30:20Z 525 - size = 324268 523 + url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl" 524 + upload-time = 2026-03-20T17:34:41Z 525 + size = 324267 526 526 527 527 [packages.wheels.hashes] 528 - sha256 = "964cf977e07f479c0697964e83deda72bcbc75c3badab506fb061b352d991b01" 528 + sha256 = "8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4" 529 529 530 530 [[packages.wheels]] 531 - url = "https://files.pythonhosted.org/packages/a4/e7/14dc9366696dcb53a413449881743426ed289d687bcf3d5aee4726c32ebb/protobuf-7.34.0-py3-none-any.whl" 532 - upload-time = 2026-02-27T00:30:23Z 533 - size = 170716 531 + url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl" 532 + upload-time = 2026-03-20T17:34:45Z 533 + size = 170715 534 534 535 535 [packages.wheels.hashes] 536 - sha256 = "e3b914dd77fa33fa06ab2baa97937746ab25695f389869afdf03e81f34e45dc7" 536 + sha256 = "bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11" 537 537 538 538 [[packages]] 539 539 name = "pydantic"
+1 -1
tiles/Cargo.toml
··· 1 1 [package] 2 2 name = "tiles" 3 - version = "0.4.4" 3 + version = "0.4.5" 4 4 edition = "2024" 5 5 6 6 [dependencies]
-1
tiles/src/commands/mod.rs
··· 255 255 } 256 256 257 257 pub async fn run(runtime: &Runtime, run_args: RunArgs) -> Result<()> { 258 - core::init().inspect_err(|e| eprintln!("Tiles core init failed due to {:?}", e))?; 259 258 runtime.run(run_args).await 260 259 } 261 260
+2 -1
tiles/src/core/accounts.rs
··· 385 385 "delete from users where user_id = ?1 and account_type != \'local\'", 386 386 [user_id], 387 387 ) { 388 - Ok(_res) => Ok(()), 388 + Ok(0) => Err(anyhow!("A peer with DID {} doesn't exist", user_id)), 389 + Ok(_) => Ok(()), 389 390 Err(err) => Err(anyhow!("Unable to unlink the peer due to {:?}", err)), 390 391 } 391 392 }
+94 -101
tiles/src/core/network/mod.rs
··· 11 11 use anyhow::Result; 12 12 use futures_util::{StreamExt, TryStreamExt}; 13 13 use iroh::{ 14 - Endpoint, EndpointId, NET_REPORT_TIMEOUT, SecretKey, 14 + Endpoint, EndpointId, NET_REPORT_TIMEOUT, PublicKey, SecretKey, 15 15 address_lookup::{self, MdnsAddressLookup, mdns}, 16 16 endpoint::{BindError, presets}, 17 17 endpoint_info::UserData, ··· 24 24 use iroh_ping::Ping; 25 25 use iroh_tickets::endpoint::EndpointTicket; 26 26 use rusqlite::Connection; 27 - use tilekit::accounts::{get_did_from_public_key, get_random_bytes, get_secret_key}; 27 + use tilekit::accounts::{ 28 + get_did_from_public_key, get_random_bytes, get_random_bytes_32, get_secret_key, 29 + }; 28 30 use tokio::task::spawn_blocking; 29 31 use uuid::Uuid; 30 32 31 33 use crate::core::{ 32 - accounts::{self, get_current_user, save_self_account_db}, 33 - network::ticket::LinkTicket, 34 + accounts::{self, get_current_user, get_user_by_user_id, save_self_account_db}, 35 + network::ticket::{EndpointUserData, LinkTicket}, 34 36 storage::db::{DBTYPE, get_db_conn}, 35 37 }; 36 38 use sha2::{Digest, Sha256}; 37 39 38 - const DEVICE_LINK_TOPIC: &str = "com.tilesprivacy.tiles.link"; 40 + const DEVICE_LINK_LOCAL_TOPIC: &str = "com.tilesprivacy.tiles.link"; 39 41 #[derive(serde::Serialize, serde::Deserialize)] 40 42 struct NetworkMessage { 43 + from_did: String, 44 + from_nickname: String, 45 + is_online: bool, 41 46 body: MessageBody, 42 - 43 47 // to prevent iroh's deduplication on same msg 44 48 nonce: [u8; 16], 45 49 } 46 50 47 51 impl NetworkMessage { 48 - fn new(body: MessageBody) -> Self { 52 + fn new(user: &accounts::User, is_online: bool, body: MessageBody) -> Self { 49 53 Self { 54 + from_did: user.user_id.clone(), 55 + from_nickname: user.username.clone(), 56 + is_online, 50 57 body, 51 58 nonce: get_random_bytes(), 52 59 } ··· 62 69 #[derive(serde::Serialize, serde::Deserialize)] 63 70 #[allow(clippy::enum_variant_names)] 64 71 enum MessageBody { 65 - LinkRequest { 66 - did: String, 67 - nickname: String, 68 - is_online: bool, 69 - ticket: String, 70 - }, 71 - LinkAccepted { 72 - did: String, 73 - nickname: String, 74 - is_online: bool, 75 - }, 76 - LinkRejected { 77 - did: String, 78 - nickname: String, 79 - is_online: bool, 80 - reason: String, 81 - }, 72 + LinkRequest { ticket: String }, 73 + LinkAccepted, 74 + LinkRejected { reason: String }, 82 75 } 83 76 84 77 // Entrypoint of network connection ··· 126 119 let mut bootstrap_ids: Vec<EndpointId> = vec![]; 127 120 // if ticket's there, then this is link enable sender's command, e;se receiver end 128 121 if let Some(ticket) = ticket { 129 - let (endpoint_id, mut did, mut nickname) = parse_link_ticket(&ticket)?; 130 - 131 - // comments while doing same machine testing 132 - 133 - // if get_user_by_user_id(&user_db_conn, &link_ticket.did).is_ok() { 134 - // println!( 135 - // "Device {}({}) already linked", 136 - // link_ticket.nickname, link_ticket.did 137 - // ); 138 - // return Ok(()); 139 - // } 122 + let (endpoint_id, mut did, mut nickname, topic_value) = parse_link_ticket(&ticket)?; 140 123 141 - let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 124 + let topic_id = if is_online { 125 + topic_value.expect("Expected topicId") 126 + } else { 127 + create_topic_id(DEVICE_LINK_LOCAL_TOPIC) 128 + }; 142 129 143 130 if is_online { 144 131 bootstrap_ids.push(endpoint_id.expect("Expected an EndpointId as bootstrapId ")) ··· 148 135 let (new_bootstrap_ids, user_data) = 149 136 find_offline_bootstrap_peers(&endpoint, mdns).await?; 150 137 bootstrap_ids = new_bootstrap_ids; 151 - let user_data_str = user_data.to_string(); 152 - let metadata_list = user_data_str.split(',').collect::<Vec<&str>>(); 153 - did = metadata_list[0].to_owned(); 154 - nickname = metadata_list[1].to_owned(); 138 + let endpoint_user_data = EndpointUserData::try_from(user_data.to_string())?; 139 + did = endpoint_user_data.did; 140 + nickname = endpoint_user_data.nickname; 155 141 }; 156 - 142 + if get_user_by_user_id(&user_db_conn, did.to_owned()).is_ok() { 143 + println!("Device {}({}) already linked", nickname, did); 144 + return Ok(()); 145 + } 157 146 let (sender, mut receiver, recv_router) = 158 147 create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; 159 148 ··· 169 158 None, 170 159 )); 171 160 172 - let link_req_msg = NetworkMessage::new(MessageBody::LinkRequest { 173 - did: user.user_id, 174 - nickname: user.username, 175 - is_online, 176 - ticket, 177 - }); 161 + let link_req_msg = 162 + NetworkMessage::new(&user, is_online, MessageBody::LinkRequest { ticket }); 178 163 sender.broadcast(link_req_msg.to_bytes().into()).await?; 179 164 180 165 println!("\nSent link request to {}({})", nickname, did); ··· 184 169 tokio::signal::ctrl_c().await?; 185 170 recv_router.shutdown().await?; 186 171 } else { 172 + // RECEIVER BLOCK 187 173 if !is_online { 188 174 let mdns = address_lookup::mdns::MdnsAddressLookup::builder().build(endpoint.id())?; 189 175 endpoint.address_lookup()?.add(mdns.clone()); 190 176 } 191 177 192 - let topic_id = create_topic_id(DEVICE_LINK_TOPIC); 178 + // Its better to have unique session'ed channels while 179 + // when the communication is over internet 180 + let topic_id = if is_online { 181 + TopicId::from_bytes(get_random_bytes_32()) 182 + } else { 183 + create_topic_id(DEVICE_LINK_LOCAL_TOPIC) 184 + }; 193 185 194 186 let (sender, receiver, recv_router) = 195 187 create_gossip_network(&endpoint, topic_id, bootstrap_ids).await?; ··· 253 245 println!("In {}:, some event {:?}", user.username, event); 254 246 } 255 247 if let Event::Received(msg) = event { 256 - match NetworkMessage::from_bytes(&msg.content)?.body { 257 - MessageBody::LinkRequest { 258 - did, 259 - nickname, 260 - is_online, 261 - ticket, 262 - } => { 248 + let pub_key = msg.delivered_from; 249 + let msg = NetworkMessage::from_bytes(&msg.content)?; 250 + if !is_did_valid(&msg.from_did, pub_key)? { 251 + eprintln!( 252 + "Incoming peer DID {} invalid, blocking request", 253 + msg.from_did 254 + ); 255 + continue; 256 + } 257 + match msg.body { 258 + MessageBody::LinkRequest { ticket } => { 263 259 println!( 264 260 "Received link request from {}({}), Do you want to link Y/N ?", 265 - nickname, did 261 + msg.from_nickname, msg.from_did 266 262 ); 267 263 let input: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new())); 268 264 ··· 277 273 278 274 let link_res_resp = if input_resp.to_lowercase() == "y" { 279 275 if let Some(gen_ticket) = &generated_ticket 280 - && !is_online 276 + && !msg.is_online 281 277 && *gen_ticket != ticket.to_lowercase() 282 278 { 283 279 println!("\nVerifying code does not match, please try again"); 284 - let response = NetworkMessage::new(MessageBody::LinkRejected { 285 - did: user.user_id.clone(), 286 - nickname: user.username.clone(), 287 - is_online, 288 - reason: String::from("Link code mismatch"), 289 - }); 280 + let response = NetworkMessage::new( 281 + &user, 282 + msg.is_online, 283 + MessageBody::LinkRejected { 284 + reason: String::from("Link code mismatch"), 285 + }, 286 + ); 290 287 sender.broadcast(response.to_bytes().into()).await?; 291 288 continue; 292 289 } 293 290 294 - if let Err(err) = save_self_account_db(&db_conn, &did, &nickname) { 291 + if let Err(err) = 292 + save_self_account_db(&db_conn, &msg.from_did, &msg.from_nickname) 293 + { 295 294 println!("Failed to add the peer locally due to {:?}", err); 296 295 297 296 continue; ··· 299 298 300 299 println!( 301 300 "Device {}({}) is now linked\nYou can exit now by ctrl-c", 302 - nickname, did 301 + msg.from_nickname, msg.from_did 303 302 ); 304 - NetworkMessage::new(MessageBody::LinkAccepted { 305 - did: user.user_id.clone(), 306 - nickname: user.username.clone(), 307 - is_online, 308 - }) 303 + NetworkMessage::new(&user, msg.is_online, MessageBody::LinkAccepted) 309 304 } else { 310 305 println!("You can exit now by ctrl-c"); 311 - NetworkMessage::new(MessageBody::LinkRejected { 312 - did: user.user_id.clone(), 313 - nickname: user.username.clone(), 314 - is_online, 315 - reason: String::from("Peer rejected the request"), 316 - }) 306 + NetworkMessage::new( 307 + &user, 308 + msg.is_online, 309 + MessageBody::LinkRejected { 310 + reason: String::from("Peer rejected the request"), 311 + }, 312 + ) 317 313 }; 318 314 input.lock().unwrap().clear(); 319 315 320 316 sender.broadcast(link_res_resp.to_bytes().into()).await?; 321 317 } 322 - MessageBody::LinkAccepted { 323 - did, 324 - nickname, 325 - is_online: _, 326 - } => { 327 - println!("\nLink accepted by {}({})", nickname, did); 318 + MessageBody::LinkAccepted => { 319 + println!("\nLink accepted by {}({})", msg.from_nickname, msg.from_did); 328 320 329 - if let Err(err) = save_self_account_db(&db_conn, &did, &nickname) { 321 + if let Err(err) = 322 + save_self_account_db(&db_conn, &msg.from_did, &msg.from_nickname) 323 + { 330 324 println!("Failed to add the peer locally due to {:?}", err); 331 325 return Ok(()); 332 326 } ··· 335 329 336 330 continue; 337 331 } 338 - MessageBody::LinkRejected { 339 - did, 340 - nickname, 341 - is_online: _, 342 - reason, 343 - } => { 332 + MessageBody::LinkRejected { reason } => { 344 333 println!( 345 334 "Oops looks like your link request has been rejected by {}({}),\nreason: {},\nexit (ctrl-c) and try again", 346 - nickname, did, reason 335 + msg.from_nickname, msg.from_did, reason 347 336 ); 348 337 } 349 338 } ··· 355 344 async fn create_endpoint(user: &accounts::User) -> Result<Endpoint> { 356 345 // In release mode, we will build the endpoint using 357 346 // tiles keypair in keychain 347 + let usr_data = EndpointUserData::new(&user.user_id, &user.username); 358 348 if !cfg!(debug_assertions) { 359 349 let signing_key = get_secret_key("tiles", &user.user_id)?; 360 - 361 350 let secret_key = SecretKey::from_bytes(&signing_key); 362 351 Endpoint::builder(presets::N0) 363 - .user_data_for_address_lookup(UserData::try_from(format!( 364 - "{},{}", 365 - user.user_id, user.username 366 - ))?) 352 + .user_data_for_address_lookup(UserData::try_from(usr_data.to_string())?) 367 353 .secret_key(secret_key) 368 354 .bind() 369 355 .await 370 356 .map_err(<BindError as Into<anyhow::Error>>::into) 371 357 } else { 372 358 Endpoint::builder(presets::N0) 373 - .user_data_for_address_lookup(UserData::try_from(format!( 374 - "{},{}", 375 - user.user_id, user.username 376 - ))?) 359 + .user_data_for_address_lookup(UserData::try_from(usr_data.to_string())?) 377 360 .bind() 378 361 .await 379 362 .map_err(<BindError as Into<anyhow::Error>>::into) ··· 453 436 454 437 // We handle the parsing in this way since ticket can be an encoded `LinkTicket` 455 438 // or just a 5 byte hex if linking over mDNS 456 - fn parse_link_ticket(ticket: &str) -> Result<(Option<EndpointId>, String, String)> { 439 + fn parse_link_ticket( 440 + ticket: &str, 441 + ) -> Result<(Option<EndpointId>, String, String, Option<TopicId>)> { 457 442 if let Ok(parsed_ticket) = LinkTicket::from_str(ticket) { 458 443 Ok(( 459 444 Some(parsed_ticket.addr.id), 460 445 parsed_ticket.did, 461 446 parsed_ticket.nickname, 447 + Some(parsed_ticket.topic_id), 462 448 )) 449 + } else if ticket.len() == 8 { 450 + // NOTE: We only have len check as a "parser" for the offline code 451 + // but this will surely change once we fix the code format 452 + Ok((None, String::from(""), String::from(""), None)) 463 453 } else { 464 - Ok((None, String::from(""), String::from(""))) 454 + Err(anyhow::anyhow!("Invalid Ticket")) 465 455 } 466 456 } 467 457 458 + fn is_did_valid(did: &str, pub_key: PublicKey) -> Result<bool> { 459 + Ok(get_did_from_public_key(&pub_key)? != did) 460 + } 468 461 // fn subsribe_mdns_events(mdns_events) {} 469 462 //TODO: Add tests, can we get some from iroh reference?
+49 -1
tiles/src/core/network/ticket.rs
··· 53 53 } 54 54 } 55 55 56 - // TODO: Add tests 56 + #[derive(serde::Serialize, serde::Deserialize, Debug)] 57 + pub struct EndpointUserData { 58 + pub did: String, 59 + pub nickname: String, 60 + } 61 + 62 + impl EndpointUserData { 63 + pub fn new(did: &str, nickname: &str) -> Self { 64 + Self { 65 + did: did.to_owned(), 66 + nickname: nickname.to_owned(), 67 + } 68 + } 69 + 70 + fn to_bytes(&self) -> Vec<u8> { 71 + postcard::to_stdvec(&self).expect("EndpointUserData to bytes couldnt be done") 72 + } 73 + } 74 + 75 + impl TryFrom<String> for EndpointUserData { 76 + type Error = anyhow::Error; 77 + fn try_from(value: String) -> Result<Self, Self::Error> { 78 + let data_bytes = data_encoding::BASE32_NOPAD.decode(value.to_uppercase().as_bytes())?; 79 + postcard::from_bytes(&data_bytes).map_err(Into::into) 80 + } 81 + } 82 + 83 + impl Display for EndpointUserData { 84 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 + let mut text = data_encoding::BASE32_NOPAD.encode(&self.to_bytes()[..]); 86 + text.make_ascii_lowercase(); 87 + write!(f, "{}", text) 88 + } 89 + } 90 + 91 + #[cfg(test)] 92 + mod tests { 93 + use crate::core::network::ticket::EndpointUserData; 94 + 95 + #[test] 96 + fn test_basic_to_fro_userdata_conversion() { 97 + let user_data = EndpointUserData::new("did:key", "machine"); 98 + let usr_data_str = user_data.to_string(); 99 + let usr_data_struct = EndpointUserData::try_from(usr_data_str).unwrap(); 100 + 101 + assert_eq!(user_data.did, usr_data_struct.did); 102 + assert_eq!(user_data.nickname, usr_data_struct.nickname); 103 + } 104 + }
+3 -2
tiles/src/main.rs
··· 2 2 3 3 use clap::{Args, Parser, Subcommand}; 4 4 use tiles::{ 5 - core::network::link, 5 + core::{self, network::link}, 6 6 daemon::{start_cmd, start_server, stop_cmd}, 7 7 runtime::{RunArgs, build_runtime}, 8 8 utils::installer, ··· 195 195 let _ = start_cmd(None).await; 196 196 }); 197 197 } 198 - 198 + core::init().inspect_err(|e| eprintln!("Tiles core init failed due to {:?}", e))?; 199 199 if !cli.flags.no_repl { 200 200 commands::run(&runtime, run_args) 201 201 .await ··· 211 211 relay_count: flags.relay_count, 212 212 memory: flags.memory, 213 213 }; 214 + core::init().inspect_err(|e| eprintln!("Tiles core init failed due to {:?}", e))?; 214 215 commands::run(&runtime, run_args) 215 216 .await 216 217 .inspect_err(|e| eprintln!("Tiles failed to run due to {:?}", e))?;