feat: Phase 9 — developer experience, extensibility, and community growth

New crates:
- quicproquo-bot: Bot SDK with polling API + JSON pipe mode
- quicproquo-kt: Key Transparency Merkle log (RFC 9162 subset)
- quicproquo-plugin-api: no_std C-compatible plugin vtable API
- quicproquo-gen: scaffolding tool (qpq-gen plugin/bot/rpc/hook)

Server features:
- ServerHooks trait wired into all RPC handlers (enqueue, fetch, auth,
  channel, registration) with plugin rejection support
- Dynamic plugin loader (libloading) with --plugin-dir config
- Delivery proof canary tokens (Ed25519 server signatures on enqueue)
- Key Transparency Merkle log with inclusion proofs on resolveUser

Core library:
- Safety numbers (60-digit HMAC-SHA256 key verification codes)
- Verifiable transcript archive (CBOR + ChaCha20-Poly1305 + hash chain)
- Delivery proof verification utility
- Criterion benchmarks (hybrid KEM, MLS, identity, sealed sender, padding)

Client:
- /verify REPL command for out-of-band key verification
- Full-screen TUI via Ratatui (feature-gated --features tui)
- qpq export / qpq export-verify CLI subcommands
- KT inclusion proof verification on user resolution

Also: ROADMAP Phase 9 added, bot SDK docs, server hooks docs,
crate-responsibilities updated, example plugins (rate_limit, logging).
This commit is contained in:
2026-03-03 22:47:38 +01:00
parent b6483dedbc
commit dc4e4e49a0
62 changed files with 6959 additions and 62 deletions

View File

@@ -0,0 +1,212 @@
use std::fs;
use std::path::Path;
pub fn generate(name: &str, output: &Path) -> Result<(), String> {
let crate_name = sanitize_name(name);
let dir = output.join(&crate_name);
if dir.exists() {
return Err(format!("directory already exists: {}", dir.display()));
}
let src_dir = dir.join("src");
fs::create_dir_all(&src_dir).map_err(|e| format!("create dir: {e}"))?;
// Cargo.toml
let cargo_toml = format!(
r#"[package]
name = "{crate_name}"
version = "0.1.0"
edition = "2021"
description = "quicproquo bot: {name}"
license = "MIT"
[dependencies]
quicproquo-bot = {{ git = "https://github.com/nickvidal/quicproquo" }}
tokio = {{ version = "1", features = ["macros", "rt-multi-thread"] }}
anyhow = "1"
tracing = "0.1"
tracing-subscriber = {{ version = "0.3", features = ["env-filter"] }}
"#,
crate_name = crate_name,
name = name,
);
write_file(&dir.join("Cargo.toml"), &cargo_toml)?;
// src/main.rs
let main_rs = format!(
r#"//! quicproquo bot: {name}
//!
//! A bot that connects to a quicproquo server and responds to messages.
//!
//! Usage:
//! {crate_name} --server 127.0.0.1:7000 --username my-bot --password secret
//!
//! Environment variables (alternative to CLI args):
//! QPQ_SERVER, QPQ_USERNAME, QPQ_PASSWORD, QPQ_CA_CERT, QPQ_STATE_PATH
use quicproquo_bot::{{Bot, BotConfig}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {{
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
// --- Configuration ---
let server = env_or("QPQ_SERVER", "127.0.0.1:7000");
let username = env_or("QPQ_USERNAME", "{crate_name}");
let password = env_or("QPQ_PASSWORD", "changeme");
let ca_cert = env_or("QPQ_CA_CERT", "server-cert.der");
let state_path = env_or("QPQ_STATE_PATH", "{crate_name}-state.bin");
let config = BotConfig::new(&server, &username, &password)
.ca_cert(&ca_cert)
.state_path(&state_path);
// --- Connect and authenticate ---
tracing::info!("connecting to {{server}} as {{username}}...");
let bot = Bot::connect(config).await?;
tracing::info!("authenticated as {{}} (key: {{}})", bot.username(), &bot.identity_key_hex()[..16]);
// --- Main loop: poll for messages and respond ---
tracing::info!("listening for messages (Ctrl+C to stop)...");
loop {{
let messages = bot.receive(5000).await?;
for msg in messages {{
tracing::info!("[{{}}] {{}}", msg.sender, msg.text);
// --- Add your command handlers here ---
if let Some(response) = handle_message(&msg.sender, &msg.text) {{
bot.send_dm(&msg.sender, &response).await?;
}}
}}
}}
}}
/// Process an incoming message and optionally return a response.
///
/// Add your bot's command logic here.
fn handle_message(sender: &str, text: &str) -> Option<String> {{
let text = text.trim();
// !help — list available commands
if text == "!help" {{
return Some(
"Available commands:\n\
!help — show this message\n\
!echo <text> — echo back the text\n\
!whoami — show your username\n\
!ping — pong!"
.to_string(),
);
}}
// !echo <text> — echo back
if let Some(rest) = text.strip_prefix("!echo ") {{
return Some(rest.to_string());
}}
// !whoami — tell the sender their username
if text == "!whoami" {{
return Some(format!("You are {{sender}}"));
}}
// !ping — respond with pong
if text == "!ping" {{
return Some("pong!".to_string());
}}
// Unknown command or regular message — no response
None
}}
fn env_or(key: &str, default: &str) -> String {{
std::env::var(key).unwrap_or_else(|_| default.to_string())
}}
"#,
name = name,
crate_name = crate_name,
);
write_file(&src_dir.join("main.rs"), &main_rs)?;
// README
let readme = format!(
r#"# {name} — quicproquo bot
## Quick start
```bash
# Build
cargo build
# Run (make sure a qpq server is running)
QPQ_SERVER=127.0.0.1:7000 \
QPQ_USERNAME={crate_name} \
QPQ_PASSWORD=changeme \
QPQ_CA_CERT=path/to/server-cert.der \
cargo run
```
## Commands
| Command | Description |
|---------|-------------|
| `!help` | Show available commands |
| `!echo <text>` | Echo back the text |
| `!whoami` | Show your username |
| `!ping` | Respond with "pong!" |
## Adding commands
Edit the `handle_message` function in `src/main.rs`:
```rust
fn handle_message(sender: &str, text: &str) -> Option<String> {{
if text == "!mycommand" {{
return Some("my response".to_string());
}}
None
}}
```
## Pipe mode
For shell integration, use the Bot SDK's JSON pipe mode:
```bash
echo '{{"action":"send","to":"alice","text":"hello"}}' | my-bot
echo '{{"action":"recv","timeout_ms":5000}}' | my-bot
```
## Documentation
- [Bot SDK docs](https://github.com/nickvidal/quicproquo/blob/main/docs/src/getting-started/bot-sdk.md)
- [Server Hooks](https://github.com/nickvidal/quicproquo/blob/main/docs/src/internals/server-hooks.md)
"#,
name = name,
crate_name = crate_name,
);
write_file(&dir.join("README.md"), &readme)?;
println!("Created bot project: {}", dir.display());
println!();
println!(" cd {crate_name}");
println!(" # Edit src/main.rs to add your commands");
println!(" QPQ_SERVER=127.0.0.1:7000 QPQ_PASSWORD=secret cargo run");
println!();
println!("The bot responds to !help, !echo, !whoami, !ping out of the box.");
Ok(())
}
fn sanitize_name(name: &str) -> String {
name.replace(['-', ' '], "_")
}
fn write_file(path: &Path, content: &str) -> Result<(), String> {
fs::write(path, content).map_err(|e| format!("write {}: {e}", path.display()))
}