Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions md/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Native MCP-over-ACP](./mcp-over-acp.md)
- [Sending Requests](./sending-requests.md)
- [Request Cancellation](./request-cancellation.md)
- [Logging and Diagnostic Privacy](./logging-privacy.md)
- [Ordered Application Dispatch](./ordered-application-dispatch.md)
- [Configurable LLM Providers](./llm-providers.md)
- [Protocol V2 and Version Routing](./protocol-v2.md)
Expand Down
27 changes: 27 additions & 0 deletions md/logging-privacy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Logging and diagnostic privacy

Ordinary SDK warning logs describe protocol errors using the numeric error code
and a fixed, code-derived reason. Dispatch warnings also identify the method
and, where applicable, request ID and handler. They do not format the error's
application-controlled message or data.

This applies to malformed typed notifications, request and notification handler
failures (including dynamic handlers), and outgoing error responses. Serde parse
reasons can quote invalid strings or enum variants, so merely omitting
`Error.data.json` would not make those reasons safe to log. The SDK does not
extract field paths from free-form error text; there is currently no separately
trusted structured field path to include in these warnings.

## Detailed diagnostics are sensitive

The errors returned to callers and sent to peers are unchanged. In particular,
`util::json_cast` and `util::json_cast_params` can include the complete failing
JSON input and a payload-bearing parse reason. Applications should not log these
errors using their ordinary `Debug` or `Display` representations unless they
intend to record that content.

Explicitly enabled protocol TRACE logging can record complete messages,
including terminal output, prompts, tool arguments, and error responses. Treat
these traces as private data and enable, store, and share them accordingly.
This warning-log policy does not redact opt-in DEBUG/TRACE diagnostics or
application-provided logging callbacks.
7 changes: 7 additions & 0 deletions src/agent-client-protocol/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

### Fixed

- Redact error messages and data in ordinary protocol warning logs, including
malformed notification parameters, handler failures, and outgoing error
responses. Preserve detailed returned errors and opt-in protocol tracing.
See [Logging and Diagnostic Privacy](../../md/logging-privacy.md).

## [3.4.0](https://github.com/agentclientprotocol/rust-sdk/compare/v3.3.0...v3.4.0) - 2026-10-16

### Added
Expand Down
1 change: 1 addition & 0 deletions src/agent-client-protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ expect-test.workspace = true
futures = { workspace = true, features = ["executor"] }
tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt", "rt-multi-thread", "sync", "time"] }
tokio-util.workspace = true
tracing-subscriber.workspace = true

[lints]
workspace = true
2 changes: 1 addition & 1 deletion src/agent-client-protocol/src/jsonrpc/close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ where
) -> Result<(), crate::Error> {
let result = (self.callback)(connection_context::from_raw::<Context, _>(connection)).await;
if let Err(error) = &result {
tracing::warn!(?error, "Connection close callback failed");
tracing::warn!(error = ?crate::util::redacted_error(error.code), "Connection close callback failed");
}
result
}
Expand Down
14 changes: 7 additions & 7 deletions src/agent-client-protocol/src/jsonrpc/incoming_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ pub(super) async fn incoming_protocol_actor<Counterpart: Role>(
}
Err(error) => {
tracing::warn!(
?error,
error = ?crate::util::redacted_error(error.code),
"Invalid transport input, sending error response"
);
let destination = destination.expect(
Expand Down Expand Up @@ -352,7 +352,7 @@ async fn handle_dynamic_handler_message<Counterpart: Role>(
new_pending_messages.push(m);
}
Err(err) => {
tracing::warn!(?err, handler = ?handler.dyn_describe_chain(), "Dynamic handler errored on pending message");
tracing::warn!(%method, err = ?crate::util::redacted_error(err.code), handler = ?handler.dyn_describe_chain(), "Dynamic handler errored on pending message");
handle_handler_error(connection, reply_target, method, err)?;
}
}
Expand Down Expand Up @@ -534,7 +534,7 @@ async fn dispatch_dispatch<Counterpart: Role>(
tracing::warn!(
?method,
?id,
?err,
err = ?crate::util::redacted_error(err.code),
"Request cancellation notification errored"
);
return handle_handler_error(connection, error_target, method, err);
Expand All @@ -559,7 +559,7 @@ async fn dispatch_dispatch<Counterpart: Role>(
}

Err(err) => {
tracing::warn!(?method, ?id, ?err, handler = ?handler.describe_chain(), "Handler errored");
tracing::warn!(?method, ?id, err = ?crate::util::redacted_error(err.code), handler = ?handler.describe_chain(), "Handler errored");
return handle_handler_error(connection, error_target, method, err);
}
}
Expand All @@ -583,7 +583,7 @@ async fn dispatch_dispatch<Counterpart: Role>(
}

Err(err) => {
tracing::warn!(?method, ?id, ?err, handler = ?dynamic_handler.dyn_describe_chain(), "Dynamic handler errored");
tracing::warn!(?method, ?id, err = ?crate::util::redacted_error(err.code), handler = ?dynamic_handler.dyn_describe_chain(), "Dynamic handler errored");
return handle_handler_error(connection, error_target, method, err);
}
}
Expand All @@ -608,7 +608,7 @@ async fn dispatch_dispatch<Counterpart: Role>(
tracing::warn!(
?method,
?id,
?err,
err = ?crate::util::redacted_error(err.code),
handler = "default",
"Default handler errored"
);
Expand Down Expand Up @@ -682,7 +682,7 @@ fn handle_handler_error<Counterpart: Role>(
None => {
tracing::warn!(
%method,
?error,
error = ?crate::util::redacted_error(error.code),
"Ignoring message-processing error because there is no request to answer"
);
Ok(())
Expand Down
10 changes: 5 additions & 5 deletions src/agent-client-protocol/src/jsonrpc/outgoing_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub(super) async fn outgoing_protocol_actor(
tracing::warn!(
?id,
%method,
?error,
error = ?crate::util::redacted_error(error.code),
"Outgoing request readiness failed"
);
if let Some(pending_reply) = pending_replies.remove(&id) {
Expand All @@ -142,7 +142,7 @@ pub(super) async fn outgoing_protocol_actor(
{
Ok(request) => request,
Err(error) => {
tracing::warn!(?id, %method, ?error, "Failed to prepare outgoing request");
tracing::warn!(?id, %method, error = ?crate::util::redacted_error(error.code), "Failed to prepare outgoing request");
if let Some(pending_reply) = pending_replies.remove(&id) {
pending_reply.fail(error);
}
Expand All @@ -168,7 +168,7 @@ pub(super) async fn outgoing_protocol_actor(
Ok(messages) => messages,
Err(error) => {
tracing::warn!(
?error,
error = ?crate::util::redacted_error(error.code),
"Dropping outgoing notification after preparation failed"
);
continue;
Expand All @@ -180,7 +180,7 @@ pub(super) async fn outgoing_protocol_actor(
Ok(message) => message,
Err(error) => {
tracing::warn!(
?error,
error = ?crate::util::redacted_error(error.code),
"Dropping outgoing notification after serialization failed"
);
continue;
Expand All @@ -203,7 +203,7 @@ pub(super) async fn outgoing_protocol_actor(
(RawJsonRpcMessage::response(id, Ok(value)), destination)
}
Err(error) => {
tracing::warn!(?id, %method, ?error, "Sending error response");
tracing::warn!(?id, %method, error = ?crate::util::redacted_error(error.code), "Sending error response");
(RawJsonRpcMessage::response(id, Err(error)), destination)
}
},
Expand Down
38 changes: 38 additions & 0 deletions src/agent-client-protocol/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,42 @@ use futures::{
mod typed;
pub use typed::{MatchDispatch, MatchDispatchFrom, TypeNotification};

/// Format only code-derived metadata at ordinary log levels.
///
/// Error messages, data, and even Serde parse reasons can contain private input.
/// Do not add fields from the original error here, including purported paths
/// extracted from its text. Full diagnostics belong in opt-in protocol tracing.
pub(crate) fn redacted_error(code: impl Into<i32>) -> impl std::fmt::Debug {
struct RedactedError {
code: i32,
reason: &'static str,
}

impl std::fmt::Debug for RedactedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Error")
.field("code", &self.code)
.field("reason", &self.reason)
.finish()
}
}

let code = code.into();
let reason = match code {
-32700 => "Parse error",
-32600 => "Invalid request",
-32601 => "Method not found",
-32602 => "Invalid params",
-32603 => "Internal error",
_ => "Protocol or application error",
};
RedactedError { code, reason }
}

/// Cast from `N` to `M` by serializing/deserialization to/from JSON.
///
/// Returned diagnostics can contain the input JSON and payload-bearing parse
/// reasons. Treat them as sensitive, as with [`json_cast_params`].
pub fn json_cast<N, M>(params: N) -> Result<M, crate::Error>
where
N: serde::Serialize,
Expand Down Expand Up @@ -36,6 +71,9 @@ where
/// [`Error::invalid_params`](`crate::Error::invalid_params`) (`-32602`)
/// instead of a parse error, which is the correct JSON-RPC error code for
/// malformed method parameters.
///
/// Returned diagnostics include the input JSON and may include input values in
/// the parse reason. Treat them as sensitive; the SDK redacts errors at WARN.
pub fn json_cast_params<N, M>(params: N) -> Result<M, crate::Error>
where
N: serde::Serialize,
Expand Down
4 changes: 2 additions & 2 deletions src/agent-client-protocol/src/util/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ impl TypeNotification {
Ok(request) => TypeNotificationState::Handled(op(request).await),
Err(error) => {
tracing::warn!(
?error,
error = ?crate::util::redacted_error(error.code),
method,
"Invalid notification params; ignoring notification without replying"
);
Expand Down Expand Up @@ -923,7 +923,7 @@ impl TypeNotification {
Ok(m) => op(m).await,
Err(error) => {
tracing::warn!(
?error,
error = ?crate::util::redacted_error(error.code),
method,
"Invalid untyped notification; ignoring notification without replying"
);
Expand Down
Loading
Loading