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
//! JSON envelope: encode (JCS-canonical) + decode + validation.

use crate::error::{Error, Result};
use serde_json::{Map, Value};

pub const VALID_TYPES: &[&str] = &[
    "req",
    "res",
    "stream_chunk",
    "stream_end",
    "cancel",
    "error",
];

/// An envelope is represented as a `serde_json::Map<String, Value>` so we can
/// preserve the field-by-field layout required for canonical JCS encoding.
pub type Envelope = Map<String, Value>;

fn require_u64(env: &Envelope, key: &str) -> Result<u64> {
    match env.get(key).and_then(|v| v.as_i64()) {
        Some(n) if n >= 0 => Ok(n as u64),
        _ => Err(Error::InvalidEnvelope(format!(
            "envelope.{key} must be a non-negative int"
        ))),
    }
}

pub fn validate(env: &Envelope) -> Result<()> {
    require_u64(env, "stream_id")?;
    require_u64(env, "seq")?;
    let t = env
        .get("type")
        .and_then(|v| v.as_str())
        .ok_or_else(|| Error::InvalidEnvelope("envelope.type missing".into()))?;
    if !VALID_TYPES.contains(&t) {
        return Err(Error::InvalidEnvelope(format!(
            "envelope.type invalid: {t}"
        )));
    }
    if let Some(c) = env.get("credits") {
        match c.as_i64() {
            Some(n) if n >= 0 => {}
            _ => {
                return Err(Error::InvalidEnvelope(
                    "envelope.credits must be a non-negative int".into(),
                ));
            }
        }
    }
    if let Some(m) = env.get("method") {
        if !m.is_string() {
            return Err(Error::InvalidEnvelope(
                "envelope.method must be a string".into(),
            ));
        }
    }
    if let Some(r) = env.get("reason") {
        if !r.is_string() {
            return Err(Error::InvalidEnvelope(
                "envelope.reason must be a string".into(),
            ));
        }
    }
    if let Some(err) = env.get("error") {
        let m = err
            .as_object()
            .ok_or_else(|| Error::InvalidEnvelope("envelope.error must be an object".into()))?;
        if !m.get("code").map(Value::is_i64).unwrap_or(false)
            || !m.get("message").map(Value::is_string).unwrap_or(false)
        {
            return Err(Error::InvalidEnvelope(
                "envelope.error must be {code:int, message:str}".into(),
            ));
        }
    }
    Ok(())
}

pub fn encode(env: &Envelope) -> Result<Vec<u8>> {
    validate(env)?;
    let v: Value = Value::Object(env.clone());
    serde_jcs::to_vec(&v).map_err(Error::Json)
}

pub fn decode(bytes: &[u8]) -> Result<Envelope> {
    let v: Value = serde_json::from_slice(bytes)?;
    let map = match v {
        Value::Object(m) => m,
        _ => {
            return Err(Error::InvalidEnvelope(
                "envelope must be a JSON object".into(),
            ))
        }
    };
    validate(&map)?;
    Ok(map)
}