Skip to main content

agent_phone/
envelope.rs

1//! JSON envelope: encode (JCS-canonical) + decode + validation.
2
3use crate::error::{Error, Result};
4use serde_json::{Map, Value};
5
6pub const VALID_TYPES: &[&str] = &[
7    "req",
8    "res",
9    "stream_chunk",
10    "stream_end",
11    "cancel",
12    "error",
13];
14
15/// An envelope is represented as a `serde_json::Map<String, Value>` so we can
16/// preserve the field-by-field layout required for canonical JCS encoding.
17pub type Envelope = Map<String, Value>;
18
19fn require_u64(env: &Envelope, key: &str) -> Result<u64> {
20    match env.get(key).and_then(|v| v.as_i64()) {
21        Some(n) if n >= 0 => Ok(n as u64),
22        _ => Err(Error::InvalidEnvelope(format!(
23            "envelope.{key} must be a non-negative int"
24        ))),
25    }
26}
27
28pub fn validate(env: &Envelope) -> Result<()> {
29    require_u64(env, "stream_id")?;
30    require_u64(env, "seq")?;
31    let t = env
32        .get("type")
33        .and_then(|v| v.as_str())
34        .ok_or_else(|| Error::InvalidEnvelope("envelope.type missing".into()))?;
35    if !VALID_TYPES.contains(&t) {
36        return Err(Error::InvalidEnvelope(format!(
37            "envelope.type invalid: {t}"
38        )));
39    }
40    if let Some(c) = env.get("credits") {
41        match c.as_i64() {
42            Some(n) if n >= 0 => {}
43            _ => {
44                return Err(Error::InvalidEnvelope(
45                    "envelope.credits must be a non-negative int".into(),
46                ));
47            }
48        }
49    }
50    if let Some(m) = env.get("method") {
51        if !m.is_string() {
52            return Err(Error::InvalidEnvelope(
53                "envelope.method must be a string".into(),
54            ));
55        }
56    }
57    if let Some(r) = env.get("reason") {
58        if !r.is_string() {
59            return Err(Error::InvalidEnvelope(
60                "envelope.reason must be a string".into(),
61            ));
62        }
63    }
64    if let Some(err) = env.get("error") {
65        let m = err
66            .as_object()
67            .ok_or_else(|| Error::InvalidEnvelope("envelope.error must be an object".into()))?;
68        if !m.get("code").map(Value::is_i64).unwrap_or(false)
69            || !m.get("message").map(Value::is_string).unwrap_or(false)
70        {
71            return Err(Error::InvalidEnvelope(
72                "envelope.error must be {code:int, message:str}".into(),
73            ));
74        }
75    }
76    Ok(())
77}
78
79pub fn encode(env: &Envelope) -> Result<Vec<u8>> {
80    validate(env)?;
81    let v: Value = Value::Object(env.clone());
82    serde_jcs::to_vec(&v).map_err(Error::Json)
83}
84
85pub fn decode(bytes: &[u8]) -> Result<Envelope> {
86    let v: Value = serde_json::from_slice(bytes)?;
87    let map = match v {
88        Value::Object(m) => m,
89        _ => {
90            return Err(Error::InvalidEnvelope(
91                "envelope must be a JSON object".into(),
92            ))
93        }
94    };
95    validate(&map)?;
96    Ok(map)
97}