feat: integrate meshservice crate into workspace

- Add meshservice to workspace members
- Fix quicprochat-client: add MeshTrace/MeshStats slash commands
- Add integration test: meshservice_tcp_transport
- Document integration points in README and docs/status.md
- Verify shared identity (IdentityKeypair → MeshAddress)
This commit is contained in:
2026-04-01 18:56:25 +02:00
parent a3023ecac1
commit 8eba12170e
8 changed files with 114 additions and 0 deletions

1
Cargo.lock generated
View File

@@ -4487,6 +4487,7 @@ dependencies = [
"hkdf", "hkdf",
"humantime-serde", "humantime-serde",
"iroh", "iroh",
"meshservice",
"quicprochat-core", "quicprochat-core",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",

View File

@@ -84,6 +84,7 @@ quicprochat/
│ ├── quicprochat-client # CLI + REPL + TUI (Ratatui) │ ├── quicprochat-client # CLI + REPL + TUI (Ratatui)
│ ├── quicprochat-kt # Key transparency (Merkle-log, revocation) │ ├── quicprochat-kt # Key transparency (Merkle-log, revocation)
│ ├── quicprochat-p2p # iroh P2P, mesh identity, store-and-forward │ ├── 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-ffi # C FFI (libquicprochat_ffi.so)
│ └── quicprochat-plugin-api # Dynamic plugin hooks (C ABI) │ └── quicprochat-plugin-api # Dynamic plugin hooks (C ABI)
├── proto/qpc/v1/ # 15 .proto schema files ├── proto/qpc/v1/ # 15 .proto schema files

View File

@@ -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 ## Features
- **Generic Protocol**: Any service can be built on top (therapy appointments, housing, repairs, tutoring...) - **Generic Protocol**: Any service can be built on top (therapy appointments, housing, repairs, tutoring...)

View File

@@ -119,6 +119,8 @@ pub enum Command {
MeshRoute, MeshRoute,
MeshIdentity, MeshIdentity,
MeshStore, MeshStore,
MeshTrace { address: String },
MeshStats,
// Security / crypto // Security / crypto
Verify { username: String }, Verify { username: String },
@@ -187,6 +189,8 @@ impl Command {
Command::MeshRoute => Some(SlashCommand::MeshRoute), Command::MeshRoute => Some(SlashCommand::MeshRoute),
Command::MeshIdentity => Some(SlashCommand::MeshIdentity), Command::MeshIdentity => Some(SlashCommand::MeshIdentity),
Command::MeshStore => Some(SlashCommand::MeshStore), 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::Verify { username } => Some(SlashCommand::Verify { username }),
Command::UpdateKey => Some(SlashCommand::UpdateKey), Command::UpdateKey => Some(SlashCommand::UpdateKey),
Command::Typing => Some(SlashCommand::Typing), Command::Typing => Some(SlashCommand::Typing),
@@ -348,6 +352,8 @@ fn slash_to_command(sc: SlashCommand) -> Command {
SlashCommand::MeshRoute => Command::MeshRoute, SlashCommand::MeshRoute => Command::MeshRoute,
SlashCommand::MeshIdentity => Command::MeshIdentity, SlashCommand::MeshIdentity => Command::MeshIdentity,
SlashCommand::MeshStore => Command::MeshStore, SlashCommand::MeshStore => Command::MeshStore,
SlashCommand::MeshTrace { address } => Command::MeshTrace { address },
SlashCommand::MeshStats => Command::MeshStats,
SlashCommand::Verify { username } => Command::Verify { username }, SlashCommand::Verify { username } => Command::Verify { username },
SlashCommand::UpdateKey => Command::UpdateKey, SlashCommand::UpdateKey => Command::UpdateKey,
SlashCommand::Typing => Command::Typing, SlashCommand::Typing => Command::Typing,
@@ -415,6 +421,8 @@ async fn execute_slash(
SlashCommand::MeshRoute => cmd_mesh_route(session), SlashCommand::MeshRoute => cmd_mesh_route(session),
SlashCommand::MeshIdentity => cmd_mesh_identity(session), SlashCommand::MeshIdentity => cmd_mesh_identity(session),
SlashCommand::MeshStore => cmd_mesh_store(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::Verify { username } => cmd_verify(session, client, &username).await,
SlashCommand::UpdateKey => cmd_update_key(session, client).await, SlashCommand::UpdateKey => cmd_update_key(session, client).await,
SlashCommand::Typing => cmd_typing(session, client).await, SlashCommand::Typing => cmd_typing(session, client).await,

View File

@@ -434,6 +434,10 @@ impl PlaybookRunner {
"mesh-route" => Ok(Command::MeshRoute), "mesh-route" => Ok(Command::MeshRoute),
"mesh-identity" | "mesh-id" => Ok(Command::MeshIdentity), "mesh-identity" | "mesh-id" => Ok(Command::MeshIdentity),
"mesh-store" => Ok(Command::MeshStore), "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}"), other => bail!("unknown command: {other}"),
} }

View File

@@ -43,6 +43,7 @@ humantime-serde = "1"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
meshservice = { path = "../meshservice" }
[[example]] [[example]]
name = "fapp_demo" name = "fapp_demo"

View File

@@ -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());
}

View File

@@ -1,5 +1,18 @@
# Status Log # 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 ## 2026-04-01 — Production Infrastructure Sprint
### Completed ### Completed