# agent-phone (Rust)
[](https://github.com/p-vbordei/agent-phone-rs/actions/workflows/ci.yml)
[](https://crates.io/crates/agent-phone)
[](./SPEC.md)
[](./LICENSE)
> **Idiomatic Rust port of [@p-vbordei/agent-phone](https://github.com/p-vbordei/agent-phone).** Minimal sync RPC between two AI agents — Noise-XK handshake over WebSocket, DID-bound peer identity, framed binary transport, self-custody keys. **Wire-format-identical** with the TS reference: the C4 hex vector matches byte-for-byte.
## What's in the box
- Noise-XK handshake (X25519 + ChaCha20Poly1305 + BLAKE2s + HKDF)
- DID-bound peer identity (Ed25519 → X25519 via SHA-512 + RFC 7748 clamp)
- Length-prefixed frame transport (ChaChaPoly seal/open)
- WebSocket client + server (`tokio-tungstenite`)
- Session lifecycle (connect, call, stream, close)
- Backpressure (credit-based streaming with auto-refresh at the half-mark)
## Install
```toml
[dependencies]
agent-phone = "0.1"
tokio = { version = "1", features = ["full"] }
```
## Quickstart
```rust
use agent_phone::session::HandlerOutput;
use agent_phone::{
connect, create_server, encode_did_key, generate_key_pair,
ClientOptions, Handler, ServerOptions,
};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let resp = generate_key_pair();
let init = generate_key_pair();
let resp_did = encode_did_key(&resp.public_key)?;
let mut handlers: HashMap<String, Handler> = HashMap::new();
handlers.insert("echo".into(), Arc::new(|p: Value| {
Box::pin(async move { Ok(HandlerOutput::Unary(p)) })
}));
let mut server = create_server(ServerOptions {
did: resp_did.clone(),
private_key: resp.private_key,
handlers,
});
server.listen(0, "127.0.0.1").await?;
let port = server.address().unwrap().port();
let client = connect(ClientOptions {
url: format!("ws://127.0.0.1:{port}"),
did: encode_did_key(&init.public_key)?,
private_key: init.private_key,
responder_did: resp_did,
responder_public_key: None,
}).await?;
println!("{}", client.call("echo", Some(serde_json::json!({"hi": 1}))).await?);
client.close().await;
server.close().await;
Ok(())
}
```
```bash
cargo run --example quickstart
# server listening on 127.0.0.1:<port>
# noise-xk handshake complete; channel authenticated
# echo result : {"hello":"agent-phone"}
# closed cleanly
```
## How it relates
| [`@p-vbordei/agent-phone`](https://github.com/p-vbordei/agent-phone) (TypeScript) | Reference | source of truth |
| [`agent-phone`](https://github.com/p-vbordei/agent-phone-py) (Python) | Port | byte-identical |
| [`agent-phone`](https://github.com/p-vbordei/agent-phone-rs) (Rust, this repo) | Port | byte-identical |
## Conformance
The [v0.1 spec](./SPEC.md) defines four conformance clauses; all four pass against the TS test suite.
- **C1 — Handshake DID-binding.** Swap the responder's static mid-handshake → initiator aborts deterministically at message 2 (AEAD fails). `tests/e2e.rs::c1_handshake_did_binding`.
- **C2 — Streaming backpressure.** Server emits 10 000 chunks, client grants 8 at a time → outstanding chunks stay bounded; every chunk arrives in order. `c2_streaming_backpressure`.
- **C3 — Graceful cancel.** Cancel mid-stream → server stops within one frame, session stays open for further RPCs. `c3_graceful_cancel`.
- **C4 — Frame determinism.** Canonical envelope bytes match the TS hex vector byte-for-byte. `tests/conformance.rs::c4_frame_determinism` reads [`vectors/c4.json`](./vectors/c4.json), which mirrors the [TS suite](https://github.com/p-vbordei/agent-phone/tree/main/conformance).
```bash
cargo test
# 21 passed
```
## Architecture
Module map, dependency choices (hand-rolled Noise XK on `chacha20poly1305` + `blake2` + `curve25519-dalek`), Ed25519→X25519 derivation gotcha, the `accept_hdr_async` quirk for capturing `?caller=<did>`, byte-determinism invariants, and testing strategy: see [docs/architecture.md](docs/architecture.md).
## Development
```bash
git clone https://github.com/p-vbordei/agent-phone-rs
cd agent-phone-rs
cargo test
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
```
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Any change to `noise.rs`, `frame.rs`, or `envelope.rs` must keep the C4 hex vector passing; that's the wire-format contract.
## License
Apache-2.0 — see [LICENSE](./LICENSE).