Skip to content

fix(a2a): stop sending exception text to the remote peer - #7317

Open
1wos wants to merge 1 commit into
google:mainfrom
1wos:fix/a2a-no-exception-text-to-peer
Open

1wos wants to merge 1 commit into
google:mainfrom
1wos:fix/a2a-no-exception-text-to-peer

Conversation

@1wos

@1wos 1wos commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:

Both executors put str(e) into the failed task status message published to the
peer:

  • src/google/adk/a2a/executor/a2a_agent_executor.py:167
  • src/google/adk/a2a/executor/a2a_agent_executor_impl.py:168

The except Exception wraps the whole of _handle_request, so it covers every
failure inside the run — model calls, tool calls, database access, file I/O — and
forwards whatever those exceptions carry. Driving the existing failure path with
exceptions of a kind a real run raises:

REMOTE PEER RECEIVES: "[Errno 2] No such file or directory:
                       '/Users/me/.config/gcloud/service-account-key.json'"
REMOTE PEER RECEIVES: 'connection failed: postgres://svc_agent:REDACTED@10.0.0.7:5432/prod'
REMOTE PEER RECEIVES: 'Incorrect API key provided: sk-proj-AbCd1234...Zz. '

A peer that only sent the agent a question receives the server's filesystem
layout, an internal host and port, a database password and part of an API key.

after_agent looks like the place to redact this, and its docstring says it covers
failed terminal events (config.py:75-79). That holds for a failure the run
produces itself: error_event becomes final_event and passes through
execute_after_agent_interceptors at a2a_agent_executor_impl.py:249. It does not
hold here, because the except block enqueues its own event directly after the
exception has abandoned _handle_request.

Solution:

  1. Generate a short error id.
  2. Log the exception in full against that id.
  3. Send the peer a fixed summary carrying only the id.

ADK_A2A_DEBUG_ERRORS=1 appends the exception text, for local debugging.

The helper lives in executor/utils.py, which both executors already import for
the interceptor helpers, so neither file grows a private copy.

# src/google/adk/a2a/executor/utils.py
def failure_summary(error: Exception) -> tuple[str, str]:
  """Returns an error id and the peer-facing text for an execution failure.

  The exception text can carry paths, hostnames and credentials, so the peer
  gets the id only. `ADK_A2A_DEBUG_ERRORS=1` appends the exception text.
  """
  error_id = uuid.uuid4().hex[:8]
  text = f'Agent execution failed. (error_id: {error_id})'
  if os.environ.get('ADK_A2A_DEBUG_ERRORS') == '1':
    text = f'{text}: {type(error).__name__}: {error}'
  return error_id, text
     except Exception as e:
-      logger.error('Error handling A2A request: %s', e, exc_info=True)
+      error_id, peer_text = failure_summary(e)
+      logger.error(
+          'Error handling A2A request [error_id=%s]: %s',
+          error_id,
+          e,
+          exc_info=True,
+      )
...
-                        parts=[_compat.make_text_part(str(e))],
+                        parts=[_compat.make_text_part(peer_text)],

With the exception text no longer leaving the process, routing this event through
execute_after_agent_interceptors becomes a consistency question rather than a
mitigation. I left it out because executor_context is built inside the try
(a2a_agent_executor_impl.py:107) and does not exist when the failure comes from
request_converter or _resolve_session, so covering it properly means hoisting
that construction. Happy to do it here if you would prefer.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

test_execute_with_exception_handling in
tests/unittests/a2a/executor/test_a2a_agent_executor_impl.py asserted that the
exception text was present in the peer's message. It now asserts that the text is
absent and the fixed summary is present.

Three tests added:

  • test_execute_failure_message_omits_exception_text in both executors' test
    files — raises FileNotFoundError on /srv/secrets/sa-key.json and asserts the
    path is absent and the whole message matches
    Agent execution failed. (error_id: [0-9a-f]{8}).
  • test_execute_failure_message_includes_detail_when_opted_in — asserts
    ADK_A2A_DEBUG_ERRORS=1 puts the exception text back.

Reverting the change in either executor makes these tests fail.

$ pytest tests/unittests/a2a/executor/ -k "exception_text or opted_in or exception_handling" -v
test_a2a_agent_executor.py::test_execute_with_exception_handling PASSED   [ 20%]
test_a2a_agent_executor.py::test_execute_failure_message_omits_exception_text PASSED [ 40%]
test_a2a_agent_executor_impl.py::test_execute_with_exception_handling PASSED [ 60%]
test_a2a_agent_executor_impl.py::test_execute_failure_message_omits_exception_text PASSED [ 80%]
test_a2a_agent_executor_impl.py::test_execute_failure_message_includes_detail_when_opted_in PASSED [100%]

================= 5 passed, 83 deselected, 3 warnings in 1.04s =================
$ pytest tests/unittests/a2a/ -q
733 passed, 49 skipped in 4.99s

$ pytest tests/unittests/a2a/ tests/unittests/tools/ tests/unittests/environment/ -q
3275 passed, 50 skipped in 43.96s

Manual End-to-End (E2E) Tests:

Not a live deployment — I drove the failure path through the executor with the
repo's own test harness on main and on this branch, raising FileNotFoundError
on a credentials path and capturing both the peer message and the log record.

On main, the peer receives the path:

PEER  : "[Errno 2] No such file or directory: '/srv/secrets/sa-key.json'"

On this branch the peer gets the id only, and the detail is in the log against
that same id, with the traceback attached:

PEER  : 'Agent execution failed. (error_id: 7aca0de3)'
LOG   : Error handling A2A request [error_id=7aca0de3]: [Errno 2] No such file or
        directory: '/srv/secrets/sa-key.json'
TRACE : yes

With ADK_A2A_DEBUG_ERRORS=1:

PEER  : "Agent execution failed. (error_id: 744ac441): FileNotFoundError:
         [Errno 2] No such file or directory: '/srv/secrets/sa-key.json'"

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

pre-commit run passes on all five touched files, including ruff, isort,
pyink, codespell and the ADK compliance checks.

The error id is 8 hex characters from uuid4, which is enough to find one request
in a log without being a value anyone would treat as a secret.

Behavior change worth knowing: a peer that previously parsed the failure text to
learn why a request failed now only gets the id. That is the point of the change,
but it is visible.

@1wos

1wos commented Sep 27, 2026

Copy link
Copy Markdown
Contributor Author

Replaces the duplicate-work question on #7315 — @chelsealong, this is the PR I mentioned there.

@1wos
1wos force-pushed the fix/a2a-no-exception-text-to-peer branch from 5617fec to 417feb1 Compare September 27, 2026 14:07
Both executors put str(e) into the failed task status message, so any file path,
hostname or credential carried by an exception raised inside the run reached the
peer that made the request.

Log the exception in full against a short error id and send the peer a fixed
summary carrying only that id. Set ADK_A2A_DEBUG_ERRORS=1 to append the exception
text when debugging locally.

The existing impl test asserted the exception text was present; it now asserts it
is absent.

Fixes google#7315
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2aAgentExecutor publishes raw exception text to the peer, bypassing the after_agent interceptor

2 participants