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};
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/// Failures are Parse errors (`-32700`). Don't use this for the params of an
112/// incoming request or notification: a peer that sends malformed params must
113/// get Invalid params (`-32602`), which [`json_cast_params`] returns. Use
114/// `json_cast` for values the SDK or your own code produced, such as response
115/// results.
116///
117/// Returned diagnostics can contain the input JSON and payload-bearing parse
118/// reasons. Treat them as sensitive, as with [`json_cast_params`].
119pub fn json_cast<N, M>(params: N) -> Result<M, crate::Error>
120where
121    N: serde::Serialize,
122    M: serde::de::DeserializeOwned,
123{
124    let json = serde_json::to_value(params).map_err(|e| {
125        crate::Error::parse_error().data(serde_json::json!({
126            "error": e.to_string(),
127            "phase": "serialization"
128        }))
129    })?;
130    let m = serde_json::from_value(json.clone()).map_err(|e| {
131        crate::Error::parse_error().data(serde_json::json!({
132            "error": e.to_string(),
133            "json": json,
134            "phase": "deserialization"
135        }))
136    })?;
137    Ok(m)
138}
139
140/// Cast incoming request/notification params into a typed payload.
141///
142/// Like [`json_cast`], but deserialization failures become
143/// [`Error::invalid_params`](`crate::Error::invalid_params`) (`-32602`)
144/// instead of a parse error, which is the correct JSON-RPC error code for
145/// malformed method parameters.
146///
147/// Returned diagnostics include the input JSON and may include input values in
148/// the parse reason. Treat them as sensitive; the SDK redacts errors at WARN.
149pub fn json_cast_params<N, M>(params: N) -> Result<M, crate::Error>
150where
151    N: serde::Serialize,
152    M: serde::de::DeserializeOwned,
153{
154    let json = serde_json::to_value(params).map_err(|e| {
155        crate::Error::internal_error().data(serde_json::json!({
156            "error": e.to_string(),
157            "phase": "serialization"
158        }))
159    })?;
160    let m = serde_json::from_value(json.clone()).map_err(|e| {
161        crate::Error::invalid_params().data(serde_json::json!({
162            "error": e.to_string(),
163            "json": json,
164            "phase": "deserialization"
165        }))
166    })?;
167    Ok(m)
168}
169
170/// An internal error (`-32603`) whose data is `message`.
171pub(crate) fn internal_error(message: impl ToString) -> crate::Error {
172    crate::Error::internal_error().data(message.to_string())
173}
174
175/// Rewrite the params of an `initialize` request for the protocol version
176/// `selected` that a version-agnostic forwarder, such as the conductor,
177/// selected for it.
178///
179/// The SDK's own agents negotiate the same way. A request for a newer version
180/// than `selected` was written for that newer version, so it keeps only what
181/// parses under `selected` instead of being rejected (for v1, only the shared
182/// capabilities are carried across). A request for `selected` or an older
183/// version keeps its params. In every case `protocolVersion` is set to
184/// `selected`.
185///
186/// # Errors
187///
188/// Returns `-32602` when `params` has no valid `protocolVersion`, and
189/// `-32600` when a newer request must be rewritten for a version this build
190/// of the SDK does not support.
191pub fn initialize_params_for_version(
192    params: &serde_json::Value,
193    selected: crate::schema::ProtocolVersion,
194) -> Result<serde_json::Map<String, serde_json::Value>, crate::Error> {
195    use crate::schema::ProtocolVersion;
196
197    let invalid_version = || {
198        crate::Error::invalid_params()
199            .data("initialize.protocolVersion must be a valid ACP protocol version")
200    };
201    let serde_json::Value::Object(object) = params else {
202        return Err(invalid_version());
203    };
204    let requested: ProtocolVersion = object
205        .get("protocolVersion")
206        .cloned()
207        .and_then(|version| serde_json::from_value(version).ok())
208        .ok_or_else(invalid_version)?;
209
210    let mut rewritten = if requested <= selected {
211        object.clone()
212    } else if selected == ProtocolVersion::V1 {
213        crate::jsonrpc::normalize_v1_initialize_request(params)?
214    } else {
215        #[cfg(feature = "unstable_protocol_v2")]
216        if selected == ProtocolVersion::V2 {
217            return crate::jsonrpc::normalize_newer_v2_initialize_request(object);
218        }
219        return Err(crate::Error::invalid_request().data(format!(
220            "cannot rewrite an initialize request for ACP protocol version {requested} to version {selected}",
221        )));
222    };
223    rewritten.insert(
224        "protocolVersion".into(),
225        serde_json::to_value(selected).map_err(crate::Error::into_internal_error)?,
226    );
227    Ok(rewritten)
228}
229
230/// The `-32600` error, sent with `id: null`, for a JSON-RPC batch array.
231///
232/// The SDK does not support JSON-RPC batching, and ACP protocol v2 removed
233/// it.
234pub(crate) fn batch_not_supported() -> crate::Error {
235    crate::Error::invalid_request()
236        .data("JSON-RPC batches are not supported; send each message individually")
237}
238
239pub(crate) fn instrumented_with_connection_name<F>(
240    name: String,
241    task: F,
242) -> tracing::instrument::Instrumented<F> {
243    use tracing::Instrument;
244
245    task.instrument(tracing::info_span!("connection", name = name))
246}
247
248pub(crate) async fn instrument_with_connection_name<R>(
249    name: Option<String>,
250    task: impl Future<Output = R>,
251) -> R {
252    if let Some(name) = name {
253        instrumented_with_connection_name(name.clone(), task).await
254    } else {
255        task.await
256    }
257}
258
259/// Run `background` until `foreground` completes.
260///
261/// Returns the result of `foreground`. If `background` errors before
262/// `foreground` completes, the error is propagated. If `background`
263/// completes with `Ok(())`, we continue waiting for `foreground`.
264pub fn run_until<T, E>(
265    background: impl Future<Output = Result<(), E>>,
266    foreground: impl Future<Output = Result<T, E>>,
267) -> impl Future<Output = Result<T, E>> {
268    use futures::future::{Either, select};
269    use std::pin::pin;
270
271    Box::pin(async move {
272        match select(pin!(background), pin!(foreground)).await {
273            Either::Left((bg_result, fg_future)) => {
274                // Background finished first
275                bg_result?; // propagate error, or if Ok(()), keep waiting
276                fg_future.await
277            }
278            Either::Right((fg_result, _bg_future)) => {
279                // Foreground finished first, drop background
280                fg_result
281            }
282        }
283    })
284}
285
286/// Process items from a stream concurrently.
287///
288/// For each item received from `stream`, calls `process_fn` to create a future,
289/// then runs all futures concurrently. If any future returns an error,
290/// stops processing and returns that error.
291///
292/// This is useful for patterns where you receive work items from a channel
293/// and want to process them concurrently while respecting backpressure.
294pub(crate) async fn process_stream_concurrently<T, F>(
295    stream: impl Stream<Item = T>,
296    process_fn: F,
297    process_fn_hack: impl for<'a> Fn(&'a F, T) -> BoxFuture<'a, Result<(), crate::Error>>,
298) -> Result<(), crate::Error>
299where
300    F: AsyncFn(T) -> Result<(), crate::Error>,
301{
302    use std::pin::pin;
303
304    use futures::stream::{FusedStream, FuturesUnordered};
305    use futures_concurrency::future::Race;
306
307    enum Event<T> {
308        NewItem(Option<T>),
309        FutureCompleted(Option<Result<(), crate::Error>>),
310    }
311
312    let mut stream = pin!(stream.fuse());
313    let mut futures: FuturesUnordered<_> = FuturesUnordered::new();
314
315    loop {
316        // If we have no futures to run, wait until we do.
317        if futures.is_empty() {
318            match stream.next().await {
319                Some(item) => futures.push(process_fn_hack(&process_fn, item)),
320                None => return Ok(()),
321            }
322            continue;
323        }
324
325        // If there are no more items coming in, just drain our queue and return.
326        if stream.is_terminated() {
327            while let Some(result) = futures.next().await {
328                result?;
329            }
330            return Ok(());
331        }
332
333        // Otherwise, race between getting a new item and completing a future.
334        let event = (async { Event::NewItem(stream.next().await) }, async {
335            Event::FutureCompleted(futures.next().await)
336        })
337            .race()
338            .await;
339
340        match event {
341            Event::NewItem(Some(item)) => {
342                futures.push(process_fn_hack(&process_fn, item));
343            }
344            Event::FutureCompleted(Some(result)) => {
345                result?;
346            }
347            Event::NewItem(None) | Event::FutureCompleted(None) => {
348                // Stream closed, loop will catch is_terminated
349                // No futures were pending, shouldn't happen since we checked is_empty
350            }
351        }
352    }
353}