fix(storyboard): default signed_requests vector transport to 'mcp' (adcp#6548) - #2540
Conversation
The runner reaches agents through tools/call (MCP, or the AdCP-over-A2A binding) — never through per-task HTTP paths — but the request-signing vector dispatch defaulted to 'raw', replaying the fixtures' recorded REST targets (/adcp/create_media_buy + raw task body) verbatim. On any MCP-transport agent every vector then 404s at the router before the agent's verifier can run (adcontextprotocol/adcp#6548), which is how the Transport Security track currently grades all-red against MCP sellers whose verifiers demonstrably work on live signed traffic. The builder's 'mcp' mode (tools/call envelope, re-signed against the actual MCP endpoint) already exists and ships in the published SDK — nothing selected it. Default the dispatch to 'mcp' via a small exported resolver; 'raw' remains an explicit opt-in (request_signing.transport) for REST-binding agents. Verified against a live MCP seller: vectors go from 39/39 routing 404s to reaching the verifier. Fixes adcontextprotocol/adcp#6548
There was a problem hiding this comment.
Ladon verdict: Request changes
Request changes — 1 blocking finding.
The fix itself is correct and well-scoped: the storyboard-runner request-signing dispatch now defaults vector transport to 'mcp' via a new exported resolver, overriding the grader's 'raw' default. Since the runner reaches agents only via tools/call, raw REST replay would 404 MCP agents by construction (adcp#6548), so the default flip is the right shape and 'raw' remains an explicit opt-in. The change is localized to the single caller and the direct grader CLI path is untouched.
Blocking findings
- src/lib/testing/storyboard/request-signing/probe-dispatch.ts:67 — Missing changeset for a published-harness default flip. Per the mandatory changeset-vs-wire-impact rule, diffs touching
src/lib/**require a.changeset/*.md; its absence is a high finding.
Add a changeset describing the transport-default behavior change and this is good to merge.
Blocking findings
- src/lib/testing/storyboard/request-signing/probe-dispatch.ts:67 — Missing changeset for published-harness default flip
|
Live-validation update, peeling the layers one at a time against an MCP seller with signature verification + signature-as-auth deployed:
Proposed follow-up (happy to implement here or in a stacked PR): in |
In mcp transport mode, the grader now performs an initialize handshake before dispatching conformance vectors and attaches Mcp-Session-Id to each probe after signing. The session header is not a covered component per RFC 9421, so signatures remain valid. Negative vectors still reach the verifier before MCP session dispatch. Follow-up to adcontextprotocol#2540 (adcp#6548 fix). Fixes the 400 Missing session ID responses that stock streamable-HTTP servers return when tools/call is POSTed without a session established by initialize. New exports: - initializeMcpSession() — acquire a session ID before grading - GradeOptions.mcpSessionId — pre-provision or disable auto-init - StoryboardRunOptions.request_signing.mcpSessionId — same for runner Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5XfTDJpfj58Dd4HbNvXnp
|
Confirmed — the proposed session-init approach is exactly right, and the implementation is ready to cherry-pick. Why the approach is sound
What's implemented Branch: Three-layer change:
To merge into this PR git fetch origin claude/pr-2540-mcp-session-init
git cherry-pick origin/claude/pr-2540-mcp-session-initOr if you'd prefer to squash: git fetch origin claude/pr-2540-mcp-session-init
git diff HEAD origin/claude/pr-2540-mcp-session-init | git applyThe changeset file ( Generated by Claude Code |
|
Reviewed the implementation on What I verified:
Blocker: The PR head is in git fetch origin claude/pr-2540-mcp-session-init
git cherry-pick origin/claude/pr-2540-mcp-session-initThe branch has two commits to pick (the session-init fix and the prettier formatting pass). The included changeset ( Generated by Claude Code |
There was a problem hiding this comment.
Ladon verdict: Comment (human reviewer recommended)
Comment — 3 medium findings, no blocking issues.
The prior blocking finding (missing changeset for the transport default flip at probe-dispatch.ts:67) is resolved — two changesets are now present. This is a net improvement over the prior request-changes run.
No critical/high findings remain. Three medium findings surfaced this run, which crosses the row 8 threshold (≥3 medium → comment). A human should look before merge.
Medium findings
- src/lib/testing/storyboard/request-signing/grader.ts:222 — Auto-init drops initializeMcpSession error; a failed handshake grades the whole batch session-less (helper doc warns this cascades to a false all-fail 400).
- src/lib/testing/storyboard/request-signing/grader.ts:440 — gradeOneVector drops initializeMcpSession error (same warn-only/dropped-error pattern as the batch path).
- test/lib/request-signing-mcp-session.test.js:36 — New tests assert only export existence, not the post-signing-injection invariant they claim to cover (missing test on a new branch).
Note: review_decision is CHANGES_REQUESTED, but gated_paths is false and there are no blocking findings, so this does not force escalate — row 8 (three mediums) governs.
Medium findings
- src/lib/testing/storyboard/request-signing/grader.ts:222 — Auto-init drops initializeMcpSession error; failed handshake grades whole batch session-less
- src/lib/testing/storyboard/request-signing/grader.ts:440 — gradeOneVector drops initializeMcpSession error (same as batch path)
- test/lib/request-signing-mcp-session.test.js:36 — New tests assert only export existence, not the post-signing injection invariant
…gent's auth An unauthenticated initialize 401s on agents that require auth (most production agents), which initializeMcpSession reads as 'stateless server' — every vector then goes session-less and the agent answers 400 Missing session ID. Thread the runner's options.auth into the handshake only; the signed vectors themselves stay bearer-less by design (the signature is their authentication). Validated live: with this, vectors progress past session handling on an auth-required MCP seller.
|
Cherry-picked your session-init branch onto this PR (fast-forward — thanks for pre-building it) and added one more validated fix on top: the Live-validation state against the MCP seller: vectors now traverse routing → signature verification → signature-as-auth → session handling. The last observed blocker was the seller's own transport rate limiter mis-bucketing bearer-less signed traffic (the rate-abuse vector's 100+ requests poisoned the per-IP window) — fixed seller-side; final full-batch verification after their deploy. |
|
Thanks for the update @fgranata — the Generated by Claude Code |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — 2 medium findings, no blocking issues, no escalation trigger.
This delta adds an initializeHeaders path so the auto-initialize MCP handshake can authenticate. The wiring is correctly scoped: the bearer header enters only the handshake, never the signed vectors, preserving the signature-is-auth invariant. Changesets are present for the src/lib changes.
Medium findings
- src/lib/testing/storyboard/request-signing/grader.ts:234 — Auto-init drops initializeMcpSession error on batch path; failed handshake grades session-less
- src/lib/testing/storyboard/request-signing/grader.ts:456 — gradeOneVector drops initializeMcpSession error (same as batch path)
Decision-table walkthrough: no critical/high findings (row 1 no). gated_paths is false, so row 2 does not fire despite review_decision=CHANGES_REQUESTED. high_risk is false (rows 3, 5 no). No data-loss/schema/infra medium categories (row 4 no). Prior decision was comment, not escalate (row 6 no). No no-auto-approve team match (row 7 no). Only 2 medium findings, fewer than 3 (row 8 no). Falls through to row 9 → approve.
The prior run's test-file finding is outside this delta and no longer applies; the two init.error-drop findings persist as Medium but are non-blocking.
Medium findings
- src/lib/testing/storyboard/request-signing/grader.ts:234 — Auto-init drops initializeMcpSession error on batch path; failed handshake grades session-less
- src/lib/testing/storyboard/request-signing/grader.ts:456 — gradeOneVector drops initializeMcpSession error (same as batch path)
|
End-to-end validation of this PR (transport default + session-init + initialize-auth threading) against a live MCP seller (adcp.bidmachine.io), full vector batch via 30 passed / 5 failed / 5 skipped (was 1/39 before the transport fix — every vector 404'd on the REST-binding replay).
Three small notes from running the full batch, none blocking this PR:
🤖 Generated with Claude Code |
|
30/5/5 against a live bidmachine instance is excellent confirmation — the core transport fix is working as intended for the storyboard-runner path (from 1/39 is a big jump). Thank you for running the full vector batch. Three notes acknowledged: Note 1 (grader.ts Note 2 (replay-window diagnostic string): Cosmetic but genuinely confusing — "all N probe pairs were accepted" when HTTP status is 401 is a misleading path. Worth a small follow-up issue. Note 3 (posture-aware split for shadow-posture verifiers): Agreed on the root cause — Generated by Claude Code |
|
Addressing Note 1 from @bokelley's review. Confirmed: both
- const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'raw' };
+ const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'mcp' };
- const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'raw' };
+ const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'mcp' };Also updated the Notes 2 and 3 correctly scoped as separate follow-ups — not touching them here. Generated by Claude Code |
|
Confirmed both 1. - const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'raw' };
+ const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'mcp' };2. - const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'raw' };
+ const buildOpts: BuildOptions = { baseUrl: agentUrl, transport: options.transport ?? 'mcp' };3. - * Transport shape the agent speaks. `'raw'` (default) POSTs per-operation
+ * Transport shape the agent speaks. `'mcp'` (default) wraps each(the rest of the JSDoc already reads correctly — just the first sentence's default label needs updating) Notes 2 and 3 from @bokelley's review are correctly scoped as separate follow-ups — not touching them here. Generated by Claude Code |
|
@fgranata — applied the three changes to git fetch upstream claude/pr-2540-grader-transport-default
git cherry-pick 55e16c89d7d07274a5c82718e9a8ad9587ea14fbOr if What the commit does (diff against
No other files touched. The only remaining Generated by Claude Code |
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
This is a subsequent review of a PR whose prior decision was approve. Both prior Medium findings have been resolved:
grader.ts:234(gradeRequestSigning) — now throws oninit.errorinstead of grading session-less.grader.ts:459(gradeOneVector) — same fix applied.
The new probe.ts helper attachMcpSessionHeader is a clean, non-mutating extraction verified by a byte-for-byte test, and initializeMcpSession now fails closed on non-2xx handshake status. Changeset is present, matching the src/lib/** change requirement. No new findings.
Decision-table walk: no critical/high findings (row 1 n/a). gated_paths is false (row 2 n/a). high_risk false, no deletions (rows 3–5 n/a). Prior decision was approve, not escalate (row 6 n/a). No no-auto-approve team match (row 7 n/a). Zero medium findings (row 8 n/a). Falls through to row 9 → approve.
Note: review_decision is REVIEW_REQUIRED, but since gated_paths is false the hard approval gate does not apply.
Both `gradeRequestSigning` and `gradeOneVector` constructed `buildOpts` with `transport: options.transport ?? 'raw'`, bypassing the `'mcp'` default already established in `probe-dispatch.ts` via `resolveVectorTransport`. A caller using the standalone grader directly against an MCP agent without an explicit `transport` option would get raw REST-target replay, routing 404 before any verifier could run. Align both call sites to `?? 'mcp'` and update the `GradeOptions.transport` JSDoc to document `'mcp'` as the default (adcp#6548 follow-up to PR adcontextprotocol#2540). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4B1ycLynivGzCS8faZ2QS
|
Cherry-picked 55e16c8 onto the branch (now 57eda3d) — 🤖 Generated with Claude Code |
Fixes adcontextprotocol/adcp#6548 — the
signed_requestsstoryboard grades all-red (39/39 routing 404s) against any MCP-transport agent, because the vector dispatch defaults totransport: 'raw'and replays the fixtures' recorded REST-binding targets (/adcp/create_media_buy, raw task body) verbatim. MCP agents serve AdCP tasks insidetools/callon a single endpoint, so the per-task paths 404 at the router before the agent's verifier is ever reached.The fix is a default, not new machinery: the builder's
'mcp'mode (tools/call envelope, re-signed against the actual target) already exists and ships in the published SDK — nothing selected it. This PR defaults the dispatch to'mcp'via a small exported resolver;'raw'remains an explicit opt-in (request_signing.transport: 'raw') for REST-binding agents. The MCP-ungradable canonicalization-edge vectors (005–008) already have their skip handling in the grader, unchanged by this PR.Verified live against an MCP seller (bidmachine,
request_signing.supported: true, verifier in production use by a signing buyer): with this change the vectors go from 39/39404to reaching the verification layer (positive vectors now surface the agent's auth semantics instead of a routing dead-end — the remaining signature-as-authentication question is agent-side and out of scope here).Test:
test/lib/request-signing-vector-transport.test.js(node:test, matches the suite convention) — default'mcp', explicit'raw'and'mcp'respected.tsc --noEmitclean; targeted test green against the built dist.