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 Crates.io Spec License

Idiomatic Rust port of @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

[dependencies]
agent-phone = "0.1"
tokio = { version = "1", features = ["full"] }

Quickstart

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(())
}
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 (TypeScript) Reference source of truth
agent-phone (Python) Port byte-identical
agent-phone (Rust, this repo) Port byte-identical

Conformance

The v0.1 spec 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, which mirrors the TS suite.
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.

Development

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. 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.