Skip to main content

agent_client_protocol/
util.rs

1// Types re-exported from crate root
2
3use futures::{
4    future::BoxFuture,
5    stream::{Stream, StreamExt},
6};
7
8mod typed;
9pub use typed::{MatchDispatch, MatchDispatchFrom, TypeNotification};
10
11/// Format only code-derived metadata at ordinary log levels.
12///
13/// Error messages, data, and even Serde parse reasons can contain private input.
14/// Do not add fields from the original error here, including purported paths
15/// extracted from its text. Full diagnostics belong in opt-in protocol tracing.
16pub(crate) fn redacted_error(code: impl Into<i32>) -> impl std::fmt::Debug {
17    struct RedactedError {
18        code: i32,
19        reason: &'static str,
20    }
21
22    impl std::fmt::Debug for RedactedError {
23        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24            f.debug_struct("Error")
25                .field("code", &self.code)
26                .field("reason", &self.reason)
27                .finish()
28        }
29    }
30
31    let code = code.into();
32    let reason = match code {
33        -32700 => "Parse error",
34        -32600 => "Invalid request",
35        -32601 => "Method not found",
36        -32602 => "Invalid params",
37        -32603 => "Internal error",
38        _ => "Protocol or application error",
39    };
40    RedactedError { code, reason }
41}
42
43/// Replace every environment variable and header value in a diagnostic copy.
44///
45/// ACP carries credentials in `env` and `headers` fields: MCP server
46/// declarations in session setup, terminal auth methods, terminal creation,
47/// and provider configuration. Names stay visible; values never reach logs.
48/// Fields are matched by name anywhere in the value rather than by method, so
49/// unrelated payloads that happen to use these keys are redacted as well.
50fn redact_env_and_header_values(value: &mut serde_json::Value) {
51    match value {
52        serde_json::Value::Object(object) => {
53            for (key, field) in object.iter_mut() {
54                if key == "env" || key == "headers" {
55                    redact_name_value_pairs(field);
56                }
57                redact_env_and_header_values(field);
58            }
59        }
60        serde_json::Value::Array(values) => {
61            values.iter_mut().for_each(redact_env_and_header_values);
62        }
63        _ => {}
64    }
65}
66
67/// Redact `[{ "name", "value" }]` lists and `{ name: value }` maps.
68fn redact_name_value_pairs(field: &mut serde_json::Value) {
69    let redacted = || serde_json::Value::String("[REDACTED]".to_owned());
70    match field {
71        serde_json::Value::Array(entries) => {
72            for entry in entries {
73                if let Some(value) = entry.get_mut("value") {
74                    *value = redacted();
75                }
76            }
77        }
78        serde_json::Value::Object(entries) => {
79            for value in entries.values_mut() {
80                *value = redacted();
81            }
82        }
83        _ => {}
84    }
85}
86
87/// Format one wire line for protocol TRACE logs with credentials redacted.
88///
89/// Redaction happens only when the event is actually formatted. A line that is
90/// not valid JSON cannot be redacted reliably, so only its length is shown.
91pub(crate) fn redacted_json_line(line: &str) -> impl std::fmt::Display + '_ {
92    struct RedactedJsonLine<'a>(&'a str);
93
94    impl std::fmt::Display for RedactedJsonLine<'_> {
95        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96            match serde_json::from_str::<serde_json::Value>(self.0) {
97                Ok(mut value) => {
98                    redact_env_and_header_values(&mut value);
99                    write!(f, "{value}")
100                }
101                Err(_) => write!(f, "<{} bytes of invalid JSON omitted>", self.0.len()),
102            }
103        }
104    }
105
106    RedactedJsonLine(line)
107}
108
109/// Cast from `N` to `M` by serializing/deserialization to/from JSON.
110///
111/// Returned diagnostics can contain the input JSON and payload-bearing parse
112/// reasons. Treat them as sensitive, as with [`json_cast_params`].
113pub fn json_cast<N, M>(params: N) -> Result<M, crate::Error>
114where
115    N: serde::Serialize,
116    M: serde::de::DeserializeOwned,
117{
118    let json = serde_json::to_value(params).map_err(|e| {
119        crate::Error::parse_error().data(serde_json::json!({
120            "error": e.to_string(),
121            "phase": "serialization"
122        }))
123    })?;
124    let m = serde_json::from_value(json.clone()).map_err(|e| {
125        crate::Error::parse_error().data(serde_json::json!({
126            "error": e.to_string(),
127            "json": json,
128            "phase": "deserialization"
129        }))
130    })?;
131    Ok(m)
132}
133
134/// Cast incoming request/notification params into a typed payload.
135///
136/// Like [`json_cast`], but deserialization failures become
137/// [`Error::invalid_params`](`crate::Error::invalid_params`) (`-32602`)
138/// instead of a parse error, which is the correct JSON-RPC error code for
139/// malformed method parameters.
140///
141/// Returned diagnostics include the input JSON and may include input values in
142/// the parse reason. Treat them as sensitive; the SDK redacts errors at WARN.
143pub fn json_cast_params<N, M>(params: N) -> Result<M, crate::Error>
144where
145    N: serde::Serialize,
146    M: serde::de::DeserializeOwned,
147{
148    let json = serde_json::to_value(params).map_err(|e| {
149        crate::Error::internal_error().data(serde_json::json!({
150            "error": e.to_string(),
151            "phase": "serialization"
152        }))
153    })?;
154    let m = serde_json::from_value(json.clone()).map_err(|e| {
155        crate::Error::invalid_params().data(serde_json::json!({
156            "error": e.to_string(),
157            "json": json,
158            "phase": "deserialization"
159        }))
160    })?;
161    Ok(m)
162}
163
164/// Creates an internal error with the given message
165pub fn internal_error(message: impl ToString) -> crate::Error {
166    crate::Error::internal_error().data(message.to_string())
167}
168
169/// Creates a parse error with the given message
170pub fn parse_error(message: impl ToString) -> crate::Error {
171    crate::Error::parse_error().data(message.to_string())
172}
173
174/// No version supported by a v2 endpoint is at or below the requested version.
175///
176/// Uses the protocol wire code so legacy v1 error models can carry the v2
177/// diagnostic as an unknown error code without changing the v1 contract.
178#[must_use]
179pub fn unsupported_protocol_version(
180    requested: crate::schema::ProtocolVersion,
181    supported: &[crate::schema::ProtocolVersion],
182) -> crate::Error {
183    crate::Error::new(-32022, "Unsupported protocol version")
184        .data(serde_json::json!({ "requested": requested, "supported": supported }))
185}
186
187pub(crate) fn instrumented_with_connection_name<F>(
188    name: String,
189    task: F,
190) -> tracing::instrument::Instrumented<F> {
191    use tracing::Instrument;
192
193    task.instrument(tracing::info_span!("connection", name = name))
194}
195
196pub(crate) async fn instrument_with_connection_name<R>(
197    name: Option<String>,
198    task: impl Future<Output = R>,
199) -> R {
200    if let Some(name) = name {
201        instrumented_with_connection_name(name.clone(), task).await
202    } else {
203        task.await
204    }
205}
206
207/// Run `background` until `foreground` completes.
208///
209/// Returns the result of `foreground`. If `background` errors before
210/// `foreground` completes, the error is propagated. If `background`
211/// completes with `Ok(())`, we continue waiting for `foreground`.
212pub fn run_until<T, E>(
213    background: impl Future<Output = Result<(), E>>,
214    foreground: impl Future<Output = Result<T, E>>,
215) -> impl Future<Output = Result<T, E>> {
216    use futures::future::{Either, select};
217    use std::pin::pin;
218
219    Box::pin(async move {
220        match select(pin!(background), pin!(foreground)).await {
221            Either::Left((bg_result, fg_future)) => {
222                // Background finished first
223                bg_result?; // propagate error, or if Ok(()), keep waiting
224                fg_future.await
225            }
226            Either::Right((fg_result, _bg_future)) => {
227                // Foreground finished first, drop background
228                fg_result
229            }
230        }
231    })
232}
233
234/// Process items from a stream concurrently.
235///
236/// For each item received from `stream`, calls `process_fn` to create a future,
237/// then runs all futures concurrently. If any future returns an error,
238/// stops processing and returns that error.
239///
240/// This is useful for patterns where you receive work items from a channel
241/// and want to process them concurrently while respecting backpressure.
242pub(crate) async fn process_stream_concurrently<T, F>(
243    stream: impl Stream<Item = T>,
244    process_fn: F,
245    process_fn_hack: impl for<'a> Fn(&'a F, T) -> BoxFuture<'a, Result<(), crate::Error>>,
246) -> Result<(), crate::Error>
247where
248    F: AsyncFn(T) -> Result<(), crate::Error>,
249{
250    use std::pin::pin;
251
252    use futures::stream::{FusedStream, FuturesUnordered};
253    use futures_concurrency::future::Race;
254
255    enum Event<T> {
256        NewItem(Option<T>),
257        FutureCompleted(Option<Result<(), crate::Error>>),
258    }
259
260    let mut stream = pin!(stream.fuse());
261    let mut futures: FuturesUnordered<_> = FuturesUnordered::new();
262
263    loop {
264        // If we have no futures to run, wait until we do.
265        if futures.is_empty() {
266            match stream.next().await {
267                Some(item) => futures.push(process_fn_hack(&process_fn, item)),
268                None => return Ok(()),
269            }
270            continue;
271        }
272
273        // If there are no more items coming in, just drain our queue and return.
274        if stream.is_terminated() {
275            while let Some(result) = futures.next().await {
276                result?;
277            }
278            return Ok(());
279        }
280
281        // Otherwise, race between getting a new item and completing a future.
282        let event = (async { Event::NewItem(stream.next().await) }, async {
283            Event::FutureCompleted(futures.next().await)
284        })
285            .race()
286            .await;
287
288        match event {
289            Event::NewItem(Some(item)) => {
290                futures.push(process_fn_hack(&process_fn, item));
291            }
292            Event::FutureCompleted(Some(result)) => {
293                result?;
294            }
295            Event::NewItem(None) | Event::FutureCompleted(None) => {
296                // Stream closed, loop will catch is_terminated
297                // No futures were pending, shouldn't happen since we checked is_empty
298            }
299        }
300    }
301}