Skip to content

fix(webhooks): add canceled/rejected/auth-required to A2A status map; fail fast on unknowns#606

Merged
bokelley merged 1 commit intomainfrom
claude/issue-603-a2a-webhook-unspecified-state
May 9, 2026
Merged

fix(webhooks): add canceled/rejected/auth-required to A2A status map; fail fast on unknowns#606
bokelley merged 1 commit intomainfrom
claude/issue-603-a2a-webhook-unspecified-state

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 8, 2026

Closes #603

Summary

create_a2a_webhook_payload silently fell back to TASK_STATE_UNSPECIFIED (proto3 integer 0) for any status not in the lookup map — canceled, rejected, auth_required, and any genuinely unknown value. MessageToDict omits proto3 zero-value fields entirely, so the wire shape became {"status": {}} with no state field. A2A v0.3 receivers that validate against the schema reject this as a missing required field.

Changes:

  • Add canceled, rejected, auth_required/auth-required to adcp_to_task_state (each has a valid pb.TaskState constant)
  • Raise ValueError for any unmapped status value (fail-closed; unknown maps to TASK_STATE_UNSPECIFIED which produces an invalid wire shape, so it is explicitly rejected with a diagnostic message)
  • Expand is_terminated to include canceled and rejected — both are terminal states per A2A v0.3, returning Task with artifacts rather than TaskStatusUpdateEvent
  • Fix pre-existing mypy no-any-return on _payload_to_dict (cast())
  • Update stale docstrings in create_a2a_webhook_payload and extract_webhook_result_data to list all four terminal states
  • Add clarifying comment: TASK_STATE_UNSPECIFIED (0) is the proto3 default and is silently omitted by MessageToDict, so it never reaches the normalization branch

Not in scope (noted by issue author): _normalize_a2a_task_state_to_v03 spec-walking gap for Task.artifacts[].parts[] — protocol expert confirmed there are no Message.role fields nested inside artifact parts in the current A2A proto schema, so no normalization gap exists there.

What tested

  • ruff check src/adcp/webhooks.py tests/test_a2a_webhook_payload.py — clean
  • mypy src/adcp/webhooks.py — no issues (also fixes pre-existing error)
  • pytest tests/test_a2a_webhook_payload.py tests/test_webhooks_deliver.py tests/test_webhook_handling.py — 141 passed, 15 skipped

New tests (tests/test_a2a_webhook_payload.py):

  • test_canceled_returns_task_with_canceled_statecanceledTask + "canceled" wire state
  • test_rejected_returns_task_with_rejected_staterejectedTask + "rejected" wire state
  • test_auth_required_returns_event_with_auth_required_stateauth_requiredTaskStatusUpdateEvent + "auth-required" wire state
  • test_unknown_status_value_raises_value_error
  • test_unknown_status_error_message_names_known_states

Pre-PR review:

  • code-reviewer: approved — no blockers; two issues addressed (inaccurate comment on underscore keys, stale docstrings listing only completed/failed as terminal)
  • ad-tech-protocol-expert: approved — protocol classification correct (canceled/rejected terminal, auth-required intermediate); two nits addressed (error message accuracy, comment clarity)

Triage-managed PR. This bot does not currently iterate on
review comments or PR conversation threads (only on the source
issue). To unblock:

  • Push fixup commits directly: gh pr checkout <num>
    fix → push.
  • Or re-trigger: comment /triage execute on the source
    issue.

See adcp#3121
for context.

Session: https://claude.ai/code/session_01S9cLUGz9Ws7FKqbeLUXxrA


Generated by Claude Code

@bokelley bokelley force-pushed the claude/issue-603-a2a-webhook-unspecified-state branch from d2daf84 to ac598ec Compare May 9, 2026 08:27
@bokelley bokelley marked this pull request as ready for review May 9, 2026 08:27
@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 9, 2026

Rebased on main now that #605 is merged. Reconciled the following:

  • Took the bot's expanded status map (canceled/rejected/auth-required) — these are valid A2A v0.3 TaskStates the original mapping omitted.
  • Took the bot's tightened status_value = status.value (signature is GeneratedTaskStatus, no string fallback needed).
  • Took the bot's expanded is_terminated check (canceled/rejected are terminal per A2A v0.3).
  • Took the bot's docstring updates listing all four terminal states.
  • Kept fix(webhooks): reject unknown AdCP status in create_a2a_webhook_payload #605's polished error message wording with the to_wire_dict reference (the bot's fixup commit referenced _payload_to_dict which no longer exists post-feat(webhooks): public to_wire_dict() serialization seam #602).
  • Removed the now-redundant test_a2a_unknown_status_raises_value_error from test_webhooks_to_wire_dict.py (it passed a raw string and would AttributeError under the tightened .value access; the bot's tests/test_a2a_webhook_payload.py covers the same case with a proper fake-enum pattern).
  • Dropped the redundant cast(dict[str, Any], MessageToDict(...)) — mypy now flags it as redundant since to_wire_dict is the public seam (feat(webhooks): public to_wire_dict() serialization seam #602).

3771 unit tests pass; ruff + mypy clean.

@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 9, 2026

Thanks for the reconciliation notes — the rebase decisions look sound. Keeping the to_wire_dict error message wording and the fake-enum test pattern over the raw-string approach are the right calls. Good to go from the triage side.


Generated by Claude Code

@bokelley bokelley closed this May 9, 2026
@bokelley bokelley force-pushed the claude/issue-603-a2a-webhook-unspecified-state branch from ac598ec to 37d2cda Compare May 9, 2026 08:29
@bokelley
Copy link
Copy Markdown
Contributor Author

bokelley commented May 9, 2026

Acknowledged — thanks for the sign-off, @bokelley.


_Generated by Claude Code


Generated by Claude Code

… fail fast on unknowns

Closes #603.

create_a2a_webhook_payload silently fell back to TASK_STATE_UNSPECIFIED
(proto3 integer 0) for any AdCP status not in the lookup map — canceled,
rejected, auth_required, and any genuinely unknown value. MessageToDict
omits proto3 zero-value fields entirely, so the wire shape became
``{"status": {}}`` with no ``state`` field. A2A v0.3 receivers that
validate against the schema reject this as a missing required field.

Changes:

- Add canceled, rejected, auth_required/auth-required to
  adcp_to_task_state (each has a valid pb.TaskState constant).
- Raise ValueError for any unmapped status value with a directional
  message naming the eight valid states. ``unknown`` has no a2a-sdk 1.0
  protobuf constant, so it is explicitly rejected; callers needing
  that wire state should build a Task manually and pass it through
  to_wire_dict.
- Expand is_terminated to include canceled and rejected — both are
  terminal states per A2A v0.3, returning Task with artifacts rather
  than TaskStatusUpdateEvent.
- Tighten status.value access (no string fallback) — the function
  signature is GeneratedTaskStatus and the docstring contract is enum
  members.
- Update stale docstrings in create_a2a_webhook_payload and
  extract_webhook_result_data to list all four terminal states.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley reopened this May 9, 2026
@bokelley bokelley merged commit 89f9491 into main May 9, 2026
16 checks passed
@bokelley bokelley deleted the claude/issue-603-a2a-webhook-unspecified-state branch May 9, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A webhook builder emits invalid 'unspecified' state when status is unknown

1 participant