diff --git a/Cargo.lock b/Cargo.lock index 58e9b55..d4c9e66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4487,6 +4487,7 @@ dependencies = [ "hkdf", "humantime-serde", "iroh", + "meshservice", "quicprochat-core", "rand 0.8.5", "serde", diff --git a/README.md b/README.md index 7ac41a2..0e76c9a 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ quicprochat/ │ ├── quicprochat-client # CLI + REPL + TUI (Ratatui) │ ├── quicprochat-kt # Key transparency (Merkle-log, revocation) │ ├── quicprochat-p2p # iroh P2P, mesh identity, store-and-forward +│ ├── meshservice # Decentralized service layer (FAPP, housing, wire format) │ ├── quicprochat-ffi # C FFI (libquicprochat_ffi.so) │ └── quicprochat-plugin-api # Dynamic plugin hooks (C ABI) ├── proto/qpc/v1/ # 15 .proto schema files diff --git a/crates/meshservice/README.md b/crates/meshservice/README.md index 6c7648d..48c86b7 100644 --- a/crates/meshservice/README.md +++ b/crates/meshservice/README.md @@ -19,6 +19,19 @@ A generic decentralized service layer for mesh networks. Build any peer-to-peer └─────────────────────────────────────────────────────────────┘ ``` +## QuicProChat / quicprochat-p2p + +This crate lives in the **product.quicproquo** workspace. Integration with the mesh stack: + +- **Ed25519 seed**: `MeshIdentity::seed_bytes()` matches `ServiceIdentity::from_secret(&seed)` (same `ed25519-dalek` derivation as `quicprochat_core::IdentityKeypair`); truncated mesh address is SHA-256(pubkey)[0..16] in both layers. +- **Example transport**: integration test `crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs` sends `wire::encode(ServiceMessage)` over `TcpTransport` (length-prefixed framing). For iroh/production, embed the same bytes in `MeshEnvelope` on ALPN `quicprochat/mesh/1`. + +Run the test from the repo root: + +```bash +cargo test -p quicprochat-p2p --test meshservice_tcp_transport +``` + ## Features - **Generic Protocol**: Any service can be built on top (therapy appointments, housing, repairs, tutoring...) diff --git a/crates/quicprochat-client/src/client/command_engine.rs b/crates/quicprochat-client/src/client/command_engine.rs index 9c0a688..63ea6f8 100644 --- a/crates/quicprochat-client/src/client/command_engine.rs +++ b/crates/quicprochat-client/src/client/command_engine.rs @@ -119,6 +119,8 @@ pub enum Command { MeshRoute, MeshIdentity, MeshStore, + MeshTrace { address: String }, + MeshStats, // Security / crypto Verify { username: String }, @@ -187,6 +189,8 @@ impl Command { Command::MeshRoute => Some(SlashCommand::MeshRoute), Command::MeshIdentity => Some(SlashCommand::MeshIdentity), Command::MeshStore => Some(SlashCommand::MeshStore), + Command::MeshTrace { address } => Some(SlashCommand::MeshTrace { address }), + Command::MeshStats => Some(SlashCommand::MeshStats), Command::Verify { username } => Some(SlashCommand::Verify { username }), Command::UpdateKey => Some(SlashCommand::UpdateKey), Command::Typing => Some(SlashCommand::Typing), @@ -348,6 +352,8 @@ fn slash_to_command(sc: SlashCommand) -> Command { SlashCommand::MeshRoute => Command::MeshRoute, SlashCommand::MeshIdentity => Command::MeshIdentity, SlashCommand::MeshStore => Command::MeshStore, + SlashCommand::MeshTrace { address } => Command::MeshTrace { address }, + SlashCommand::MeshStats => Command::MeshStats, SlashCommand::Verify { username } => Command::Verify { username }, SlashCommand::UpdateKey => Command::UpdateKey, SlashCommand::Typing => Command::Typing, @@ -415,6 +421,8 @@ async fn execute_slash( SlashCommand::MeshRoute => cmd_mesh_route(session), SlashCommand::MeshIdentity => cmd_mesh_identity(session), SlashCommand::MeshStore => cmd_mesh_store(session), + SlashCommand::MeshTrace { address } => cmd_mesh_trace(session, &address), + SlashCommand::MeshStats => cmd_mesh_stats(session), SlashCommand::Verify { username } => cmd_verify(session, client, &username).await, SlashCommand::UpdateKey => cmd_update_key(session, client).await, SlashCommand::Typing => cmd_typing(session, client).await, diff --git a/crates/quicprochat-client/src/client/playbook.rs b/crates/quicprochat-client/src/client/playbook.rs index 1716bff..2b7234b 100644 --- a/crates/quicprochat-client/src/client/playbook.rs +++ b/crates/quicprochat-client/src/client/playbook.rs @@ -434,6 +434,10 @@ impl PlaybookRunner { "mesh-route" => Ok(Command::MeshRoute), "mesh-identity" | "mesh-id" => Ok(Command::MeshIdentity), "mesh-store" => Ok(Command::MeshStore), + "mesh-trace" => Ok(Command::MeshTrace { + address: self.resolve_str(&step.args, "address")?, + }), + "mesh-stats" => Ok(Command::MeshStats), other => bail!("unknown command: {other}"), } diff --git a/crates/quicprochat-p2p/Cargo.toml b/crates/quicprochat-p2p/Cargo.toml index 73c5d61..ab49fd7 100644 --- a/crates/quicprochat-p2p/Cargo.toml +++ b/crates/quicprochat-p2p/Cargo.toml @@ -43,6 +43,7 @@ humantime-serde = "1" [dev-dependencies] tempfile = "3" +meshservice = { path = "../meshservice" } [[example]] name = "fapp_demo" diff --git a/crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs b/crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs new file mode 100644 index 0000000..3462bd1 --- /dev/null +++ b/crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs @@ -0,0 +1,73 @@ +//! Integration: [`meshservice`] wire payloads over [`quicprochat_p2p::transport_tcp::TcpTransport`]. +//! +//! Demonstrates that the same Ed25519 seed backs both [`MeshIdentity`] (P2P) and +//! [`meshservice::identity::ServiceIdentity`], so service-layer signatures verify after +//! hop-across-TCP. Production mesh would use [`MeshEnvelope`] / iroh; this test keeps +//! the transport boundary explicit. + +use meshservice::capabilities; +use meshservice::identity::ServiceIdentity; +use meshservice::router::ServiceRouter; +use meshservice::services::fapp::{create_announce, FappService, Modality, SlotAnnounce, Specialism}; +use meshservice::wire; +use quicprochat_p2p::address::MeshAddress; +use quicprochat_p2p::identity::MeshIdentity; +use quicprochat_p2p::transport::MeshTransport; +use quicprochat_p2p::transport_tcp::TcpTransport; + +#[tokio::test] +async fn meshservice_fapp_over_tcp_roundtrip() { + let seed = [0x5eu8; 32]; + let mesh = MeshIdentity::from_seed(seed); + let service = ServiceIdentity::from_secret(&seed); + + assert_eq!(mesh.public_key(), service.public_key()); + assert_eq!( + *MeshAddress::from_public_key(&mesh.public_key()).as_bytes(), + service.address() + ); + + let announce = SlotAnnounce::new( + &[Specialism::CognitiveBehavioral], + Modality::VideoCall, + "803", + ) + .with_slots(2); + + let msg = create_announce(&service, &announce, 1).expect("create_announce"); + let frame = wire::encode(&msg).expect("wire encode"); + + let transport = TcpTransport::bind("127.0.0.1:0") + .await + .expect("bind tcp"); + let dest = transport.transport_addr(); + + let recv = tokio::spawn(async move { transport.recv().await.expect("recv") }); + let send_transport = TcpTransport::bind("127.0.0.1:0") + .await + .expect("bind sender"); + send_transport + .send(&dest, &frame) + .await + .expect("send"); + + let packet = recv.await.expect("join recv"); + let decoded = wire::decode(&packet.data).expect("wire decode"); + assert!(decoded.verify(&service.public_key())); + assert_eq!(decoded.service_id, meshservice::service_ids::FAPP); + + let mut router = ServiceRouter::new(capabilities::RELAY); + router.register(Box::new(FappService::relay())); + let action = router + .handle(decoded, Some(service.public_key())) + .expect("router handle"); + assert!( + matches!( + action, + meshservice::router::ServiceAction::Store + | meshservice::router::ServiceAction::StoreAndForward + ), + "unexpected action: {action:?}" + ); + assert!(!router.store().is_empty()); +} diff --git a/docs/status.md b/docs/status.md index 167c38e..a204eda 100644 --- a/docs/status.md +++ b/docs/status.md @@ -1,5 +1,18 @@ # Status Log +## 2026-04-01 — meshservice workspace integration + +### Completed +- **Workspace** — `crates/meshservice/` is a workspace member (`Cargo.toml`); `cargo check -p meshservice` and full `cargo check --workspace` succeed. +- **P2P bridge test** — `crates/quicprochat-p2p/tests/meshservice_tcp_transport.rs`: same Ed25519 seed for `MeshIdentity` and `meshservice::ServiceIdentity`; FAPP announce encoded with `meshservice::wire`, sent over `TcpTransport`, decoded and handled by `ServiceRouter` + `FappService::relay()`. +- **Client command engine** — `SlashCommand::MeshTrace` / `MeshStats` wired through `Command` and `execute_slash` (fixes non-exhaustive match); playbook steps `mesh-trace` / `mesh-stats` added. + +### Integration notes +- **Transport**: `meshservice` is transport-agnostic; carry `wire::encode` bytes inside `MeshEnvelope` / mesh ALPN (`quicprochat/mesh/1`) for production — not yet a direct dependency from `quicprochat-p2p` lib code. +- **FAPP duplication**: `quicprochat-p2p::fapp` (legacy mesh FAPP) and `meshservice::services::fapp` (generic service layer) coexist; long-term alignment TBD. + +--- + ## 2026-04-01 — Production Infrastructure Sprint ### Completed