Follow-up from #1547 (re-review). Untyped exceptions surface raw str(e) to the buyer, unchanged by normalization. Pre-existing — the synchronous failed-Task artifact already carried it; #1547 only extended it to the top-level webhook body — so this is not a #1547 gate, but the durable fix belongs at the shared sink.
Detail
normalize_to_adcp_error (src/core/exceptions.py:988) maps an untyped Exception to AdCPError(str(exc)) — it rewrites the wire code (INTERNAL_ERROR → SERVICE_UNAVAILABLE) but preserves the message verbatim. build_two_layer_error_envelope passes exc.message into both envelope layers (:951-954), and the A2A path also forwards it to the webhook body (_fail_task_with_webhook(task, str(e)), src/a2a_server/adcp_a2a_server.py:984 → result_data["error"]). For a SQLAlchemy/OS error, str(e) can carry SQL fragments, table names, or filesystem paths.
AdCP transport-errors.mdx §Security Considerations directs that buyer-facing error messages not include DB text / stack traces / internal paths.
Contract
Sanitize at the sink: for an untyped Exception normalized to base AdCPError(INTERNAL_ERROR), emit a generic buyer-facing message and log the raw str(e) server-side only. This closes the failed-Task artifact, the webhook body, and the per-skill result paths in one place, across all transports (MCP/REST/A2A share normalize_to_adcp_error). Verify against the wire-envelope tests + BDD error scenarios.
Interacts with #1578 (the protocol-webhook channel posts to a client-supplied URL without SSRF validation), so an unsanitized message can reach an attacker-chosen endpoint.
Follow-up from #1547 (re-review). Untyped exceptions surface raw
str(e)to the buyer, unchanged by normalization. Pre-existing — the synchronous failed-Task artifact already carried it; #1547 only extended it to the top-level webhook body — so this is not a #1547 gate, but the durable fix belongs at the shared sink.Detail
normalize_to_adcp_error(src/core/exceptions.py:988) maps an untypedExceptiontoAdCPError(str(exc))— it rewrites the wire code (INTERNAL_ERROR→SERVICE_UNAVAILABLE) but preserves the message verbatim.build_two_layer_error_envelopepassesexc.messageinto both envelope layers (:951-954), and the A2A path also forwards it to the webhook body (_fail_task_with_webhook(task, str(e)),src/a2a_server/adcp_a2a_server.py:984→result_data["error"]). For a SQLAlchemy/OS error,str(e)can carry SQL fragments, table names, or filesystem paths.AdCP
transport-errors.mdx§Security Considerations directs that buyer-facing error messages not include DB text / stack traces / internal paths.Contract
Sanitize at the sink: for an untyped
Exceptionnormalized to baseAdCPError(INTERNAL_ERROR), emit a generic buyer-facing message and log the rawstr(e)server-side only. This closes the failed-Task artifact, the webhook body, and the per-skill result paths in one place, across all transports (MCP/REST/A2A sharenormalize_to_adcp_error). Verify against the wire-envelope tests + BDD error scenarios.Interacts with #1578 (the protocol-webhook channel posts to a client-supplied URL without SSRF validation), so an unsanitized message can reach an attacker-chosen endpoint.