agent_first_http/shared/
envelope.rs1use serde::Serialize;
9use std::io::Write;
10
11use crate::shared::error::Error;
12
13pub fn emit<W: Write, T: Serialize>(writer: &mut W, code: &str, payload: &T) -> Result<(), Error> {
21 emit_inner(writer, code, payload, RedactionMode::Default)
22}
23
24pub fn emit_unredacted<W: Write, T: Serialize>(
27 writer: &mut W,
28 code: &str,
29 payload: &T,
30) -> Result<(), Error> {
31 emit_inner(writer, code, payload, RedactionMode::None)
32}
33
34#[derive(Clone, Copy)]
35enum RedactionMode {
36 Default,
37 None,
38}
39
40fn emit_inner<W: Write, T: Serialize>(
41 writer: &mut W,
42 code: &str,
43 payload: &T,
44 redaction: RedactionMode,
45) -> Result<(), Error> {
46 let mut value = serde_json::to_value(payload).map_err(|e| {
47 Error::new(
48 crate::shared::error::ErrorCode::InternalError,
49 format!("envelope: failed to serialize payload: {e}"),
50 )
51 })?;
52 value = wrap_payload(code, value)?;
53
54 let line = match redaction {
55 RedactionMode::Default => agent_first_data::output_json(&value),
56 RedactionMode::None => agent_first_data::output_json_with(
57 &value,
58 agent_first_data::RedactionPolicy::RedactionNone,
59 ),
60 };
61 writer.write_all(line.as_bytes())?;
62 writer.write_all(b"\n")?;
63 Ok(())
64}
65
66fn wrap_payload(code: &str, value: serde_json::Value) -> Result<serde_json::Value, Error> {
67 let serde_json::Value::Object(mut map) = value else {
68 return Err(Error::new(
69 crate::shared::error::ErrorCode::InternalError,
70 "envelope: payload must serialize to a JSON object",
71 ));
72 };
73
74 match map.get("code") {
75 Some(serde_json::Value::String(existing)) if existing == code => {}
76 Some(_) => {
77 return Ok(serde_json::json!({
78 "code": code,
79 "result": serde_json::Value::Object(map),
80 }));
81 }
82 None => {
83 map.insert("code".into(), serde_json::Value::String(code.to_string()));
84 }
85 }
86 Ok(serde_json::Value::Object(map))
87}
88
89pub fn emit_error<W: Write>(writer: &mut W, err: &Error) -> Result<(), Error> {
91 emit(writer, "error", err)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[derive(Serialize)]
99 struct HealthPayload {
100 status: &'static str,
101 uptime_s: u64,
102 }
103
104 #[test]
105 fn json_envelope_is_single_line_with_code_field() {
106 let mut buf = Vec::new();
107 let payload = HealthPayload {
108 status: "ok",
109 uptime_s: 42,
110 };
111 emit(&mut buf, "health", &payload).unwrap();
112 let s = String::from_utf8(buf).unwrap_or_default();
113 assert!(s.ends_with('\n'));
114 let trimmed = s.trim_end();
115 let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
116 assert_eq!(parsed["code"], "health");
117 assert_eq!(parsed["status"], "ok");
118 assert_eq!(parsed["uptime_s"], 42);
119 assert_eq!(trimmed.lines().count(), 1);
120 }
121
122 #[test]
123 fn error_envelope_uses_error_code_tag() {
124 let mut buf = Vec::new();
125 let err = Error::new(
126 crate::shared::error::ErrorCode::NavigationTimeout,
127 "no load",
128 );
129 emit_error(&mut buf, &err).unwrap();
130 let parsed: serde_json::Value =
131 serde_json::from_slice(&buf).unwrap_or(serde_json::Value::Null);
132 assert_eq!(parsed["code"], "error");
133 assert_eq!(parsed["error_code"], "navigation_timeout");
134 assert_eq!(parsed["error"], "no load");
135 assert_eq!(parsed["retryable"], true);
136 }
137
138 #[derive(Serialize)]
139 struct SecretPayload {
140 token_secret: &'static str,
141 }
142
143 #[test]
144 fn envelope_redacts_afdata_secret_fields() {
145 let mut buf = Vec::new();
146 emit(
147 &mut buf,
148 "container_status",
149 &SecretPayload {
150 token_secret: "supersecret",
151 },
152 )
153 .unwrap();
154 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
155 assert_eq!(parsed["token_secret"], "***");
156 }
157
158 #[test]
159 fn envelope_wraps_payload_when_code_would_collide() {
160 let mut buf = Vec::new();
161 let payload = serde_json::json!({"code": -32000, "message": "cdp error"});
162 emit(&mut buf, "cdp", &payload).unwrap();
163 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
164 assert_eq!(parsed["code"], "cdp");
165 assert_eq!(parsed["result"]["code"], -32000);
166 }
167}