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:
233
docs/src/getting-started/bot-sdk.md
Normal file
233
docs/src/getting-started/bot-sdk.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Bot SDK
|
||||
|
||||
The `quicproquo-bot` crate provides a high-level SDK for building automated
|
||||
agents on the quicproquo network. Bots authenticate with OPAQUE, send and
|
||||
receive E2E encrypted messages through MLS, and can be driven programmatically
|
||||
or via a JSON pipe interface for shell integration.
|
||||
|
||||
---
|
||||
|
||||
## Adding the dependency
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
quicproquo-bot = { path = "../crates/quicproquo-bot" }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
```rust,no_run
|
||||
use quicproquo_bot::{Bot, BotConfig};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = BotConfig::new("127.0.0.1:7000", "bot-user", "bot-password")
|
||||
.ca_cert("server-cert.der")
|
||||
.state_path("bot-state.bin");
|
||||
|
||||
let bot = Bot::connect(config).await?;
|
||||
|
||||
// Send a DM
|
||||
bot.send_dm("alice", "Hello from bot!").await?;
|
||||
|
||||
// Poll for messages
|
||||
loop {
|
||||
for msg in bot.receive(5000).await? {
|
||||
println!("{}: {}", msg.sender, msg.text);
|
||||
if msg.text.starts_with("!echo ") {
|
||||
bot.send_dm(&msg.sender, &msg.text[6..]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
`BotConfig` uses a builder pattern. The only required arguments are the server
|
||||
address, username, and password:
|
||||
|
||||
```rust,no_run
|
||||
# use quicproquo_bot::BotConfig;
|
||||
let config = BotConfig::new("127.0.0.1:7000", "my-bot", "secret123")
|
||||
.ca_cert("certs/server-cert.der") // TLS CA certificate (DER format)
|
||||
.server_name("my-server.example") // TLS SNI (default: "localhost")
|
||||
.state_path("my-bot-state.bin") // Persistent state file
|
||||
.state_password("encrypt-me") // State file encryption password
|
||||
.device_id("bot-device-1"); // Device identifier
|
||||
```
|
||||
|
||||
| Method | Default | Description |
|
||||
|-------------------|-----------------------|-------------|
|
||||
| `ca_cert()` | `"server-cert.der"` | Path to the server's CA certificate in DER format. |
|
||||
| `server_name()` | `"localhost"` | TLS server name for certificate validation. |
|
||||
| `state_path()` | `"bot-state.bin"` | Path to the bot's encrypted state file. |
|
||||
| `state_password()` | None (unencrypted) | Password for encrypting the state file at rest. |
|
||||
| `device_id()` | None | Device ID reported to the server in auth tokens. |
|
||||
|
||||
---
|
||||
|
||||
## Sending messages
|
||||
|
||||
```rust,no_run
|
||||
# use quicproquo_bot::Bot;
|
||||
# async fn example(bot: &Bot) -> anyhow::Result<()> {
|
||||
// Send a plaintext DM — encryption is handled internally via MLS
|
||||
bot.send_dm("alice", "Hello!").await?;
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
`send_dm` resolves the username, establishes or joins the MLS group for the DM
|
||||
channel, encrypts the plaintext, and delivers it through the server. Each call
|
||||
opens a fresh QUIC connection (stateless reconnect pattern).
|
||||
|
||||
---
|
||||
|
||||
## Receiving messages
|
||||
|
||||
```rust,no_run
|
||||
# use quicproquo_bot::Bot;
|
||||
# async fn example(bot: &Bot) -> anyhow::Result<()> {
|
||||
// Wait up to 5 seconds for pending messages
|
||||
let messages = bot.receive(5000).await?;
|
||||
for msg in &messages {
|
||||
println!("[seq={}] {}: {}", msg.seq, msg.sender, msg.text);
|
||||
}
|
||||
|
||||
// For binary/non-UTF-8 content, use receive_raw
|
||||
let raw_messages = bot.receive_raw(5000).await?;
|
||||
for payload in &raw_messages {
|
||||
println!("received {} bytes", payload.len());
|
||||
}
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
The `Message` struct contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------|----------|-------------|
|
||||
| `sender` | `String` | The sender's username. |
|
||||
| `text` | `String` | Decrypted plaintext content (UTF-8). |
|
||||
| `seq` | `u64` | Sequence number. |
|
||||
|
||||
---
|
||||
|
||||
## Resolving users
|
||||
|
||||
```rust,no_run
|
||||
# use quicproquo_bot::Bot;
|
||||
# async fn example(bot: &Bot) -> anyhow::Result<()> {
|
||||
let identity_key = bot.resolve_user("alice").await?;
|
||||
println!("alice's identity key: {} bytes", identity_key.len());
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Identity inspection
|
||||
|
||||
```rust,no_run
|
||||
# use quicproquo_bot::Bot;
|
||||
# fn example(bot: &Bot) {
|
||||
println!("username: {}", bot.username());
|
||||
println!("identity key (hex): {}", bot.identity_key_hex());
|
||||
let raw_key: [u8; 32] = bot.identity_key();
|
||||
# }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pipe mode (stdin/stdout JSON lines)
|
||||
|
||||
For shell integration, the bot SDK supports a JSON-lines pipe interface. Each
|
||||
line on stdin is a JSON command; results are written to stdout as JSON lines.
|
||||
|
||||
### Supported actions
|
||||
|
||||
**Send a message:**
|
||||
|
||||
```json
|
||||
{"action": "send", "to": "alice", "text": "hello from pipe"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"status": "ok", "action": "send"}
|
||||
```
|
||||
|
||||
**Receive pending messages:**
|
||||
|
||||
```json
|
||||
{"action": "recv", "timeout_ms": 5000}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"status": "ok", "messages": [{"sender": "peer", "text": "hi", "seq": 0}]}
|
||||
```
|
||||
|
||||
**Resolve a username:**
|
||||
|
||||
```json
|
||||
{"action": "resolve", "username": "alice"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"status": "ok", "identity_key": "ab12cd34..."}
|
||||
```
|
||||
|
||||
### Error responses
|
||||
|
||||
All actions return an error object on failure:
|
||||
|
||||
```json
|
||||
{"error": "OPAQUE login: connection refused"}
|
||||
```
|
||||
|
||||
### Shell examples
|
||||
|
||||
```bash
|
||||
# Send via pipe
|
||||
echo '{"action":"send","to":"alice","text":"hello"}' | my-bot-binary
|
||||
|
||||
# Receive via pipe
|
||||
echo '{"action":"recv","timeout_ms":5000}' | my-bot-binary
|
||||
|
||||
# Use with jq for pretty output
|
||||
echo '{"action":"recv","timeout_ms":3000}' | my-bot-binary | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture notes
|
||||
|
||||
- **Stateless reconnect**: Each `send_dm` and `receive` call opens a fresh QUIC
|
||||
connection. There is no persistent connection to manage.
|
||||
- **MLS encryption**: All messages are end-to-end encrypted via MLS (RFC 9420).
|
||||
The bot SDK wraps the client library's `cmd_send` and
|
||||
`receive_pending_plaintexts` functions.
|
||||
- **State persistence**: The bot's identity seed and MLS group state are stored
|
||||
in the state file. Losing this file means losing the bot's identity.
|
||||
- **Cap'n Proto !Send**: RPC calls run on a `tokio::task::LocalSet` because
|
||||
`capnp-rpc` is `!Send`.
|
||||
|
||||
---
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Running the Client](running-the-client.md) -- CLI subcommands and REPL
|
||||
- [Server Hooks](../internals/server-hooks.md) -- extend the server with plugins
|
||||
- [Demo Walkthrough](demo-walkthrough.md) -- step-by-step messaging scenario
|
||||
Reference in New Issue
Block a user