agent-phone 0.1.0

Minimal sync RPC between two AI agents (Rust port of @p-vbordei/agent-phone). Self-custody keys, Noise-framework handshake, DID-bound WebSocket.
Documentation
# agent-phone (Rust)

[![CI](https://github.com/p-vbordei/agent-phone-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/p-vbordei/agent-phone-rs/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/agent-phone.svg)](https://crates.io/crates/agent-phone)
[![Spec](https://img.shields.io/badge/spec-v0.1-blue)](./SPEC.md)
[![License](https://img.shields.io/badge/license-Apache%202.0-green)](./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

| Implementation | Status | Wire format |
|---|---|---|
| [`@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).