molecule-core/workspace-server
Hongming Wang 140fc5fb10 fix(a2a): v0.2 → v0.3 compat shim at proxy edge (#2345)
Closes #2345.

## Symptom

Design Director silently dropped A2A briefs whose sender used the
v0.2 message format (`params.message.content` string) instead of v0.3
(`params.message.parts` part-list). The downstream a2a-sdk's v0.3
Pydantic validator rejected with "params.message.parts — Field
required" but the rejection only landed in tenant-side logs; the
sender saw HTTP 200/202 and assumed delivery.

UX Researcher therefore never received the kickoff. Multi-agent
pipeline silently idle.

## Fix

Convert at the proxy edge in normalizeA2APayload. Two cases handled,
one explicitly rejected:

  v0.2 string content   → wrap as [{kind: text, text: <content>}]
                          (the canonical v0.2 case from the dogfooding
                          report)
  v0.2 list content     → preserve list as parts (some older clients
                          put a list under `content`; treat as "client
                          meant parts, used wrong field name")
  v0.3 parts present    → no-op (hot path for normal traffic)
  Neither present       → return HTTP 400 with structured JSON-RPC
                          error pointing at the missing field

Why at the proxy edge: every workspace gets the compat for free
without each one bumping a2a-sdk separately. The SDK's own compat
adapter is strict about `parts` and rejects v0.2 senders.

Why reject loud on missing-both: pre-fix the SDK's Pydantic
rejection was post-handler-dispatch and invisible to the original
sender. Now misshapen payloads return a structured 400 to the actual
caller — kills the entire silent-drop class for this payload-shape
category.

## Tests

7 new cases on normalizeA2APayload (#2345) + 1 fixture update on the
existing _MissingMethodReturnsEmpty test:

  TestNormalizeA2APayload_ConvertsV02StringContentToParts
  TestNormalizeA2APayload_ConvertsV02ListContentToParts
  TestNormalizeA2APayload_PreservesV03Parts (hot path)
  TestNormalizeA2APayload_RejectsMessageWithNeitherContentNorParts
  TestNormalizeA2APayload_RejectsContentWithUnsupportedType
  TestNormalizeA2APayload_NoMessageNoCheck (e.g. tasks/list bypasses)

All 11 normalizeA2APayload tests pass + full handler suite (no
regressions).

## Refs

Hard-gates discussion: this is exactly the class of failure
(silent-drop on schema mismatch) that #2342 (continuous synthetic
E2E) would catch automatically. Tier 2 RFC item from #2345 (caller
gets structured JSON-RPC error on parse failure) is delivered above
via the loud-reject path.
2026-04-29 22:01:41 -07:00
..
cmd/server feat(runtime): native_scheduler skip — primitive #3 of 6 2026-04-26 22:47:00 -07:00
internal fix(a2a): v0.2 → v0.3 compat shim at proxy edge (#2345) 2026-04-29 22:01:41 -07:00
migrations feat(wsauth): platform→workspace inbound secret (RFC #2312, PR-A) 2026-04-29 14:09:33 -07:00
pkg/provisionhook feat(#1957): wire gh-identity plugin into workspace-server 2026-04-24 15:01:41 +00:00
.ci-force chore: force Platform(Go) CI run on main — validate go vet clean 2026-04-21 15:43:19 +00:00
.gitignore feat(ws-server): pull env from CP on startup 2026-04-19 02:41:15 -07:00
.golangci.yaml chore(workspace-server): add golangci.yaml disabling errcheck 2026-04-24 07:16:54 +00:00
Dockerfile chore: extract ContextMenu Zustand fix + a2a_proxy local-docker SSRF bypass + workspace-server Dockerfile GID entrypoint 2026-04-22 20:00:16 -07:00
Dockerfile.tenant feat(terminal): remote path via aws ec2-instance-connect + pty 2026-04-21 18:13:29 -07:00
entrypoint-tenant.sh fix(security): add USER directive before ENTRYPOINT in all tenant images (#1155) 2026-04-20 23:51:33 +00:00
go.mod chore(deps): batch dep bumps — 11 safe upgrades from 2026-04-28 dependabot wave 2026-04-28 16:25:46 -07:00
go.sum chore(deps): batch dep bumps — 11 safe upgrades from 2026-04-28 dependabot wave 2026-04-28 16:25:46 -07:00