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