1use serde::Serialize;
8use serde::de::DeserializeOwned;
9use std::io::Write;
10use std::sync::OnceLock;
11
12use crate::shared::error::Error;
13
14static OUTPUT_TO: OnceLock<agent_first_data::OutputTo> = OnceLock::new();
15
16pub fn install_output_to(selector: agent_first_data::OutputTo) -> Result<(), Error> {
22 OUTPUT_TO.set(selector).map_err(|_| {
23 Error::new(
24 crate::shared::error::ErrorCode::InternalError,
25 "AFDATA output routing was initialized more than once",
26 )
27 })
28}
29
30#[must_use]
31pub fn output_to() -> agent_first_data::OutputTo {
32 OUTPUT_TO
33 .get()
34 .copied()
35 .unwrap_or(agent_first_data::OutputTo::Split)
36}
37
38pub fn emit<W: Write, T: Serialize>(writer: &mut W, code: &str, payload: &T) -> Result<(), Error> {
45 emit_inner(writer, code, payload, RedactionMode::Default)
46}
47
48pub fn emit_unredacted<W: Write, T: Serialize>(
51 writer: &mut W,
52 code: &str,
53 payload: &T,
54) -> Result<(), Error> {
55 emit_inner(writer, code, payload, RedactionMode::None)
56}
57
58pub fn emit_process<T: Serialize>(code: &str, payload: &T) -> Result<(), Error> {
60 emit_process_inner(code, payload, RedactionMode::Default)
61}
62
63pub fn emit_process_unredacted<T: Serialize>(code: &str, payload: &T) -> Result<(), Error> {
68 emit_process_inner(code, payload, RedactionMode::None)
69}
70
71pub fn emit_process_revealing_takeover<T: Serialize>(code: &str, payload: &T) -> Result<(), Error> {
75 let value = prepare_revealed_takeover_payload(code, payload)?;
76 emit_process_value(value, RedactionMode::None)
77}
78
79#[derive(Clone, Copy)]
80enum RedactionMode {
81 Default,
82 None,
83}
84
85fn emit_inner<W: Write, T: Serialize>(
86 writer: &mut W,
87 code: &str,
88 payload: &T,
89 redaction: RedactionMode,
90) -> Result<(), Error> {
91 let value = prepare_payload(code, payload)?;
92
93 let options = output_options(redaction);
94 let mut emitter = agent_first_data::CliEmitter::with_options(
95 writer,
96 agent_first_data::OutputFormat::Json,
97 options,
98 )
99 .with_strict_protocol();
100 emitter.emit_result(value).map_err(|err| {
101 Error::new(
102 crate::shared::error::ErrorCode::InternalError,
103 err.to_string(),
104 )
105 })?;
106 Ok(())
107}
108
109fn emit_process_inner<T: Serialize>(
110 code: &str,
111 payload: &T,
112 redaction: RedactionMode,
113) -> Result<(), Error> {
114 emit_process_value(prepare_payload(code, payload)?, redaction)
115}
116
117fn emit_process_value(value: serde_json::Value, redaction: RedactionMode) -> Result<(), Error> {
118 let mut emitter = agent_first_data::CliEmitter::from_output_to_with(
119 output_to(),
120 agent_first_data::OutputFormat::Json,
121 output_options(redaction),
122 )
123 .with_strict_protocol();
124 emitter.emit_result(value).map_err(|err| {
125 Error::new(
126 crate::shared::error::ErrorCode::InternalError,
127 err.to_string(),
128 )
129 })
130}
131
132fn output_options(redaction: RedactionMode) -> agent_first_data::OutputOptions {
133 match redaction {
134 RedactionMode::Default => agent_first_data::OutputOptions {
135 redaction: agent_first_data::Redactor::new(),
136 style: agent_first_data::PlainStyle::Raw,
137 },
138 RedactionMode::None => agent_first_data::OutputOptions {
139 redaction: agent_first_data::Redactor::new()
140 .policy(agent_first_data::RedactionPolicy::Off),
141 style: agent_first_data::PlainStyle::Raw,
142 },
143 }
144}
145
146fn prepare_payload<T: Serialize>(code: &str, payload: &T) -> Result<serde_json::Value, Error> {
147 let value = serde_json::to_value(payload).map_err(|e| {
148 Error::new(
149 crate::shared::error::ErrorCode::InternalError,
150 format!("AFDATA: failed to serialize payload: {e}"),
151 )
152 })?;
153 wrap_payload(code, value).map(|value| crate::shared::redact::redact_url_fields(&value))
154}
155
156fn prepare_revealed_takeover_payload<T: Serialize>(
157 code: &str,
158 payload: &T,
159) -> Result<serde_json::Value, Error> {
160 let original = prepare_payload(code, payload)?;
161 let mut redacted = agent_first_data::Redactor::new().value(&original);
162 restore_named_field(&original, &mut redacted, "takeover_url_secret");
163 Ok(redacted)
164}
165
166fn restore_named_field(
167 original: &serde_json::Value,
168 redacted: &mut serde_json::Value,
169 field_name: &str,
170) {
171 match (original, redacted) {
172 (serde_json::Value::Object(original), serde_json::Value::Object(redacted)) => {
173 for (key, original_value) in original {
174 let Some(redacted_value) = redacted.get_mut(key) else {
175 continue;
176 };
177 if key == field_name {
178 *redacted_value = original_value.clone();
179 } else {
180 restore_named_field(original_value, redacted_value, field_name);
181 }
182 }
183 }
184 (serde_json::Value::Array(original), serde_json::Value::Array(redacted)) => {
185 for (original, redacted) in original.iter().zip(redacted.iter_mut()) {
186 restore_named_field(original, redacted, field_name);
187 }
188 }
189 _ => {}
190 }
191}
192
193fn wrap_payload(code: &str, value: serde_json::Value) -> Result<serde_json::Value, Error> {
194 let serde_json::Value::Object(mut map) = value else {
195 return Err(Error::new(
196 crate::shared::error::ErrorCode::InternalError,
197 "AFDATA result payload must serialize to a JSON object",
198 ));
199 };
200
201 map.insert("code".into(), serde_json::Value::String(code.to_string()));
202 Ok(serde_json::Value::Object(map))
203}
204
205pub fn emit_error<W: Write>(writer: &mut W, err: &Error) -> Result<(), Error> {
207 let mut emitter = agent_first_data::CliEmitter::with_options(
208 writer,
209 agent_first_data::OutputFormat::Json,
210 output_options(RedactionMode::Default),
211 )
212 .with_strict_protocol();
213 let event = agent_first_data::json_error(err.error_code.as_str(), &err.detail)
214 .retryable_if(err.retryable)
215 .build()
216 .map_err(|err| {
217 Error::new(
218 crate::shared::error::ErrorCode::InternalError,
219 err.to_string(),
220 )
221 })?;
222 emitter.emit(event).map_err(|emit_err| {
223 Error::new(
224 crate::shared::error::ErrorCode::InternalError,
225 emit_err.to_string(),
226 )
227 })
228}
229
230pub fn emit_process_error(err: &Error) -> Result<(), Error> {
232 let event = agent_first_data::json_error(err.error_code.as_str(), &err.detail)
233 .retryable_if(err.retryable)
234 .build()
235 .map_err(|error| {
236 Error::new(
237 crate::shared::error::ErrorCode::InternalError,
238 error.to_string(),
239 )
240 })?;
241 let mut emitter = agent_first_data::CliEmitter::from_output_to_with(
242 output_to(),
243 agent_first_data::OutputFormat::Json,
244 output_options(RedactionMode::Default),
245 )
246 .with_strict_protocol();
247 emitter.emit(event).map_err(|error| {
248 Error::new(
249 crate::shared::error::ErrorCode::InternalError,
250 error.to_string(),
251 )
252 })
253}
254
255pub fn emit_error_with<W: Write>(
257 writer: &mut W,
258 code: &str,
259 message: &str,
260 fields: serde_json::Value,
261 trace: serde_json::Value,
262) -> Result<(), Error> {
263 let mut emitter = agent_first_data::CliEmitter::with_options(
264 writer,
265 agent_first_data::OutputFormat::Json,
266 output_options(RedactionMode::Default),
267 )
268 .with_strict_protocol();
269 let retryable = fields
270 .get("retryable")
271 .and_then(serde_json::Value::as_bool)
272 .unwrap_or(false);
273 let fields = crate::shared::redact::redact_url_fields(&fields);
274 let trace = crate::shared::redact::redact_url_fields(&trace);
275 let fields = match fields {
276 serde_json::Value::Object(mut fields) => {
277 fields.remove("retryable");
278 serde_json::Value::Object(fields)
279 }
280 other => other,
281 };
282 let event = agent_first_data::json_error(code, message)
283 .retryable_if(retryable)
284 .fields(fields)
285 .trace(trace)
286 .build()
287 .map_err(|err| {
288 Error::new(
289 crate::shared::error::ErrorCode::InternalError,
290 err.to_string(),
291 )
292 })?;
293 emitter.emit(event).map_err(|err| {
294 Error::new(
295 crate::shared::error::ErrorCode::InternalError,
296 err.to_string(),
297 )
298 })
299}
300
301pub fn emit_process_error_with(
303 code: &str,
304 message: &str,
305 fields: serde_json::Value,
306 trace: serde_json::Value,
307) -> Result<(), Error> {
308 let retryable = fields
309 .get("retryable")
310 .and_then(serde_json::Value::as_bool)
311 .unwrap_or(false);
312 let fields = crate::shared::redact::redact_url_fields(&fields);
313 let trace = crate::shared::redact::redact_url_fields(&trace);
314 let fields = match fields {
315 serde_json::Value::Object(mut fields) => {
316 fields.remove("retryable");
317 serde_json::Value::Object(fields)
318 }
319 other => other,
320 };
321 let event = agent_first_data::json_error(code, message)
322 .retryable_if(retryable)
323 .fields(fields)
324 .trace(trace)
325 .build()
326 .map_err(|error| {
327 Error::new(
328 crate::shared::error::ErrorCode::InternalError,
329 error.to_string(),
330 )
331 })?;
332 let mut emitter = agent_first_data::CliEmitter::from_output_to_with(
333 output_to(),
334 agent_first_data::OutputFormat::Json,
335 output_options(RedactionMode::Default),
336 )
337 .with_strict_protocol();
338 emitter.emit(event).map_err(|error| {
339 Error::new(
340 crate::shared::error::ErrorCode::InternalError,
341 error.to_string(),
342 )
343 })
344}
345
346pub fn error_value(code: &str, message: &str, retryable: bool) -> serde_json::Value {
348 let value: serde_json::Value = agent_first_data::json_error(code, message)
349 .retryable_if(retryable)
350 .build()
351 .map(Into::into)
352 .unwrap_or_else(|_| serde_json::json!({}));
353 crate::shared::redact::redact_value(&value)
354}
355
356pub fn result_value(code: &str, mut payload: serde_json::Value) -> serde_json::Value {
358 let payload = match &mut payload {
359 serde_json::Value::Object(fields) => {
360 fields
361 .entry("code".to_string())
362 .or_insert_with(|| serde_json::Value::String(code.to_string()));
363 payload
364 }
365 _ => serde_json::json!({"code": code, "value": payload}),
366 };
367 let value: serde_json::Value = agent_first_data::json_result(payload).build().into();
368 crate::shared::redact::redact_value(&value)
369}
370
371pub fn result_value_revealing_takeover(
374 code: &str,
375 mut payload: serde_json::Value,
376) -> serde_json::Value {
377 let payload = match &mut payload {
378 serde_json::Value::Object(fields) => {
379 fields
380 .entry("code".to_string())
381 .or_insert_with(|| serde_json::Value::String(code.to_string()));
382 payload
383 }
384 _ => serde_json::json!({"code": code, "value": payload}),
385 };
386 let original: serde_json::Value = agent_first_data::json_result(payload).build().into();
387 let original = crate::shared::redact::redact_url_fields(&original);
388 let mut redacted = agent_first_data::Redactor::new().value(&original);
389 restore_named_field(&original, &mut redacted, "takeover_url_secret");
390 redacted
391}
392
393pub fn decode_result<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, Error> {
394 let text = std::str::from_utf8(bytes).map_err(|error| {
395 Error::new(
396 crate::shared::error::ErrorCode::InternalError,
397 format!("decode AFDATA event: {error}"),
398 )
399 })?;
400 match agent_first_data::decode_protocol_event(text) {
401 Ok(agent_first_data::DecodedEvent::Result(result)) => serde_json::from_value(result.result)
402 .map_err(|error| {
403 Error::new(
404 crate::shared::error::ErrorCode::InternalError,
405 format!("decode AFDATA result payload: {error}"),
406 )
407 }),
408 Ok(_) => Err(Error::new(
409 crate::shared::error::ErrorCode::InternalError,
410 "expected AFDATA result event",
411 )),
412 Err(error) => Err(Error::new(
413 crate::shared::error::ErrorCode::InternalError,
414 format!("invalid AFDATA event: {error}"),
415 )),
416 }
417}
418
419pub fn decode_error(bytes: &[u8]) -> Result<Error, Error> {
420 let text = std::str::from_utf8(bytes).map_err(|error| {
421 Error::new(
422 crate::shared::error::ErrorCode::InternalError,
423 format!("decode AFDATA error event: {error}"),
424 )
425 })?;
426 match agent_first_data::decode_protocol_event(text) {
427 Ok(agent_first_data::DecodedEvent::Error(error)) => {
428 let code = serde_json::from_value(serde_json::Value::String(error.code))
429 .unwrap_or(crate::shared::error::ErrorCode::InternalError);
430 Ok(Error::new(code, error.message).with_retryable(error.retryable))
431 }
432 Ok(_) => Err(Error::new(
433 crate::shared::error::ErrorCode::InternalError,
434 "expected AFDATA error event",
435 )),
436 Err(error) => Err(Error::new(
437 crate::shared::error::ErrorCode::InternalError,
438 format!("invalid AFDATA error event: {error}"),
439 )),
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[derive(Serialize)]
448 struct HealthPayload {
449 status: &'static str,
450 uptime_s: u64,
451 }
452
453 #[test]
454 fn json_result_event_is_single_line_with_code_field() {
455 let mut buf = Vec::new();
456 let payload = HealthPayload {
457 status: "ok",
458 uptime_s: 42,
459 };
460 emit(&mut buf, "health", &payload).unwrap();
461 let s = String::from_utf8(buf).unwrap_or_default();
462 assert!(s.ends_with('\n'));
463 let trimmed = s.trim_end();
464 let parsed: serde_json::Value = serde_json::from_str(trimmed).unwrap();
465 assert_eq!(parsed["kind"], "result");
466 assert_eq!(parsed["result"]["code"], "health");
467 assert_eq!(parsed["result"]["status"], "ok");
468 assert_eq!(parsed["result"]["uptime_s"], 42);
469 assert_eq!(trimmed.lines().count(), 1);
470 }
471
472 #[test]
473 fn error_event_uses_error_code_tag() {
474 let mut buf = Vec::new();
475 let err = Error::new(
476 crate::shared::error::ErrorCode::NavigationTimeout,
477 "no load",
478 );
479 emit_error(&mut buf, &err).unwrap();
480 let parsed: serde_json::Value =
481 serde_json::from_slice(&buf).unwrap_or(serde_json::Value::Null);
482 assert_eq!(parsed["kind"], "error");
483 assert_eq!(parsed["error"]["code"], "navigation_timeout");
484 assert_eq!(parsed["error"]["message"], "no load");
485 assert_eq!(parsed["error"]["retryable"], true);
486 }
487
488 #[test]
489 fn error_extension_fields_are_flattened_into_error_payload() {
490 let mut buf = Vec::new();
491 emit_error_with(
492 &mut buf,
493 "navigation_timeout",
494 "no load",
495 serde_json::json!({
496 "retryable": true,
497 "stage": "capture_text",
498 "details": "scalar detail remains an explicitly named field"
499 }),
500 serde_json::json!({"duration_ms": 10}),
501 )
502 .unwrap();
503 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
504 assert_eq!(parsed["error"]["stage"], "capture_text");
505 assert_eq!(parsed["error"]["retryable"], true);
506 assert_eq!(
507 parsed["error"]["details"],
508 "scalar detail remains an explicitly named field"
509 );
510 assert!(parsed["error"].get("fields").is_none());
511 }
512
513 #[derive(Serialize)]
514 struct SecretPayload {
515 token_secret: &'static str,
516 }
517
518 #[test]
519 fn afdata_event_redacts_secret_fields() {
520 let mut buf = Vec::new();
521 emit(
522 &mut buf,
523 "container_status",
524 &SecretPayload {
525 token_secret: "supersecret",
526 },
527 )
528 .unwrap();
529 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
530 assert_eq!(parsed["result"]["token_secret"], "***");
531 }
532
533 #[test]
534 fn http_result_redacts_common_url_query_credentials() {
535 let value = result_value(
536 "fetch",
537 serde_json::json!({
538 "request_url": "https://user:pass@example.test/?token=abc&safe=ok"
539 }),
540 );
541 assert_eq!(
542 value["result"]["request_url"],
543 "https://user:***@example.test/?token=***&safe=ok"
544 );
545 }
546
547 #[test]
548 fn takeover_result_reveals_only_the_explicit_capability() {
549 let value = result_value_revealing_takeover(
550 "takeover_handoff",
551 serde_json::json!({
552 "takeover_url_secret":
553 "https://example.test/takeover?handoff_secret=short-lived",
554 "host_token_secret": "long-lived",
555 "request_url": "https://example.test/?token=also-secret"
556 }),
557 );
558 assert_eq!(
559 value["result"]["takeover_url_secret"],
560 "https://example.test/takeover?handoff_secret=short-lived"
561 );
562 assert_eq!(value["result"]["host_token_secret"], "***");
563 assert_eq!(
564 value["result"]["request_url"],
565 "https://example.test/?token=***"
566 );
567 }
568
569 #[test]
570 fn envelope_uses_sdk_result_payload_without_nested_envelope() {
571 let mut buf = Vec::new();
572 let payload = serde_json::json!({"code": -32000, "message": "cdp error"});
573 emit(&mut buf, "cdp", &payload).unwrap();
574 let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
575 assert_eq!(parsed["kind"], "result");
576 assert_eq!(parsed["result"]["code"], "cdp");
577 assert_eq!(parsed["result"]["code"], "cdp");
578 assert_eq!(parsed["result"]["message"], "cdp error");
579 assert!(parsed["result"].get("result").is_none());
580 }
581}