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