Skip to content

fix(runtime): use lowercase wire role for v0.3 JSON-RPC compat layer - #2187

Merged
hongmingwang-moleculeai merged 1 commit into
stagingfrom
fix/mcp-server-path-wheel-relative
Apr 27, 2026
Merged

fix(runtime): use lowercase wire role for v0.3 JSON-RPC compat layer#2187
hongmingwang-moleculeai merged 1 commit into
stagingfrom
fix/mcp-server-path-wheel-relative

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

Summary

  • Restores cross-workspace delegation. Once fix(runtime): legacy /app/ path leaks across MCP server + agent prompts + docstrings #2186 fixed the MCP path so `delegate_task` could actually fire, every cross-workspace call came back as JSON-RPC `-32600` "Invalid Request" with the underlying pydantic ValidationError:

    ```
    params.message.role
    Input should be 'agent' or 'user' [type=enum,
    input_value='ROLE_USER', input_type=str]
    ```

  • PR fix: pass rpc_url='/' to create_jsonrpc_routes (a2a-sdk 1.x) #2184's a2a-sdk 1.x sweep over-corrected: it changed every `"role": "user"` literal in JSON-RPC payload construction to `"role": "ROLE_USER"` to match the protobuf enum names of the 1.x native types. That was correct for in-process `Message` construction (which the SDK serialises before the wire) but wrong for the 8 sites that hand-build JSON-RPC payloads. The workspace's own a2a-sdk runs inbound requests through the v0.3 compat adapter (`enable_v0_3_compat=True` in main.py), and that adapter validates against the v0.3 Pydantic Role enum (`agent`|`user` lowercase). The protobuf-style names blow it up.

  • Reverted the 8 wire-payload sites to lowercase `"user"`. Native-type usage at `a2a_executor.py:471` (`Role.ROLE_AGENT`) stays — that's an in-process `Message` construction; the SDK handles wire serialisation correctly. Updated the misleading comment at `main.py:255-257`.

  • Regression test: `test_jsonrpc_wire_role_format.py` greps the 6 wire-payload-emitting files for any `"ROLE_USER"` / `"ROLE_AGENT"` string literal and fails loud — cheapest possible drift detector.

Why E2E missed it

The priority-runtimes harness sends a single message canvas → workspace, but the canvas already used lowercase `"user"` (it never went through the migration sweep). The bug only surfaces on workspace → workspace delegation, which the harness doesn't exercise. Same gap as #131.

Test plan

  • New regression test passes
  • Manual reproduction in canvas: Design Director's `delegate_task → Perf Auditor` returns the JSON-RPC -32600 error (before fix)
  • Same flow succeeds end-to-end (after merge + cascade)
  • CI green

🤖 Generated with Claude Code

Manual-test failure surfaced what was hidden behind the MCP-path bug:
once delegate_task could actually fire, every cross-workspace call
came back as JSON-RPC -32600 "Invalid Request" with the underlying
pydantic ValidationError:

    params.message.role
      Input should be 'agent' or 'user' [type=enum,
      input_value='ROLE_USER', input_type=str]

PR #2184's a2a-sdk 1.x migration sweep over-corrected: it changed
every `"role": "user"` literal in JSON-RPC payload construction to
`"role": "ROLE_USER"` to match the protobuf enum names of the 1.x
native types (a2a.types.Role.ROLE_USER / ROLE_AGENT). That was
correct for in-process Message construction (which the SDK
serialises before wire transmission) but WRONG for the 8 sites that
hand-build JSON-RPC payloads. The workspace's own a2a-sdk runs
inbound requests through the v0.3 compat adapter
(/usr/local/lib/python3.11/site-packages/a2a/compat/v0_3/) because
main.py sets enable_v0_3_compat=True for backwards compatibility,
and that adapter validates against the v0.3 Pydantic Role enum
(`agent` | `user` lowercase). The protobuf-style names blow it up.

Reverted the 8 wire-payload sites to lowercase:
  - workspace/a2a_client.py:74
  - workspace/a2a_cli.py:74, 111
  - workspace/heartbeat.py:378
  - workspace/main.py:464, 563
  - workspace/builtin_tools/a2a_tools.py:60
  - workspace/builtin_tools/delegation.py:272

Native-type usage at workspace/a2a_executor.py:471 (`Role.ROLE_AGENT`)
stays — that's an in-process Message construction; the SDK handles
wire serialisation correctly.

Updated the misleading comment at main.py:255-257 (which said
"outbound payloads are now 1.x-shaped (ROLE_USER)") to spell out
the actual rule: outbound JSON-RPC wire payloads MUST use v0.3
shape, native types are only for in-process construction.

New regression test test_jsonrpc_wire_role_format.py greps the 6
wire-payload-emitting files for any "ROLE_USER" / "ROLE_AGENT"
string literal and fails loud — cheapest possible drift detector.

Why E2E missed it: the priority-runtimes harness sends a single
message canvas → workspace, but the canvas already used lowercase
"user" (it never went through the migration sweep). The bug only
surfaces on workspace → workspace delegation, which the harness
doesn't exercise. Same gap as #131 (extend smoke to call main()
against a stub).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged via the queue into staging with commit e0a35a3 Apr 27, 2026
14 checks passed
HongmingWang-Rabbit pushed a commit that referenced this pull request Jun 12, 2026
…t into ensureDefaultConfig (provider field) — fixes canvas moonshot/kimi-k2.6 NOT_CONFIGURED' (#2187) from fix/ensure-default-config-stamp-derived-provider into main
HongmingWang-Rabbit pushed a commit that referenced this pull request Jun 12, 2026
…t/kimi NOT_CONFIGURED

The moonshot/kimi incident: a canvas-created claude-code workspace with
provider=Platform + model=moonshot/kimi-k2.6 booted NOT_CONFIGURED in prod
because the generated config.yaml lacked the manifest-derived `provider:`
key, so the adapter slash-split "moonshot/..." -> unregistered provider.
Fixed by #2187 (ensureDefaultConfig stamps DeriveProvider->provider:platform)
+ #2188 (canvas). Unit tests passed; the REAL boot path was the gap.

This adds comprehensive regression coverage so the CLASS cannot reship:

Deterministic (no live infra, runs in the normal unit suite):
  workspace-server/internal/handlers/workspace_provision_platform_boot_test.go
  - TestEnsureDefaultConfig_StampsProviderForEverySSOTPlatformModel:
    enumerates the claude-code `platform` arm from the providers SSOT
    (providers.LoadManifest) and asserts ensureDefaultConfig stamps
    provider:platform (top-level AND runtime_config) for EVERY offered
    platform model — not just the single moonshot/kimi pin #2187 shipped.
    A newly-offered platform model gets a case for free and only passes if
    actually stamped (closes the offered-but-not-stamped divergence the bug
    rode in on). Mutation-verified: disabling the stamp fails the test.
  - TestPlatformModelDeriveProvider_SSOTConsistency: the upstream half —
    DeriveProvider maps every SSOT platform model to provider Name "platform".

Real-boot (staging; I will run it):
  Extends the existing staging harness (no new harness) with a
  platform-managed path: E2E_LLM_PATH=platform pin-selects moonshot/kimi-k2.6,
  sends NO tenant key, and reuses the harness's online-wait + completion
  assertions to prove the workspace reaches status=online (not
  not_configured) and a completion returns 200. The BYOK branches never
  exercised the platform arm — the exact arm the bug shipped on.
  - tests/e2e/lib/model_slug.sh: platform path + override semantics
  - tests/e2e/test_model_slug.sh: 4 new pinned cases (16/16 green)
  - tests/e2e/test_staging_full_saas.sh: empty-secrets platform branch
  - .gitea/workflows/e2e-staging-saas.yml: new `E2E Staging Platform Boot`
    job (continue-on-error during de-flake; bp-required: pending #2187),
    + providers.yaml/model_slug.sh added to the path triggers.

Coverage-audit theme: mc#1982 (continue-on-error masks; de-flake-then-gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HongmingWang-Rabbit pushed a commit that referenced this pull request Jun 12, 2026
…+ local-provision port-squatter flake

- reduced-motion.test.ts: the connection-status pulse dot moved from
  SidePanel.tsx into the extracted WorkspacePanelTabs.tsx; retarget the
  motion-safe:animate-pulse assertion to where the guarded indicator now
  lives (was the only red in CI / Canvas -> gates CI / all-required).
- e2e-staging-saas.yml: add bp directives to the 4 new concierge jobs the
  Tier-2g lint flagged — bp-required: pending #2430 for the three real
  push-time staging e2e jobs (creates-workspace / platform / user-tasks,
  aspiring gates sharing the cp#245 de-flake surface), bp-exempt for the
  PR-time compile-only job. #2187 (the sibling's tracker) is closed/unrelated.
- local-provision-e2e.yml (no-flakes RCA): the :8080 kill-step only matched
  procs *named* platform-server, so a differently-named squatter survived,
  our bind went FATAL, and the /health loop false-positived against the
  squatter. Free :8080 from ANY holder (fuser/lsof) and verify our own PID
  owns the port BEFORE trusting /health, in both the stub and real jobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant