fix(server): adcpError two-layer emission for AdCP error arms (#1606)#1610
Merged
fix(server): adcpError two-layer emission for AdCP error arms (#1606)#1610
Conversation
…ayers (#1606) Eighteen AdCP response schemas declare a typed Error arm requiring `errors: [{code, message}, ...]` at the payload layer alongside the envelope-layer `{adcp_error: {...}}`. `adcpError()` emitted only the envelope; `wrapErrorArm` emitted only the payload. Both paths now ship the two-layer wire shape the spec has required since 3.0.6. Implementation per RFC `docs/proposals/adcperror-two-layer-emission.md` (Option C — framework auto-wraps at the dispatcher finalize seam). Adopters keep calling `adcpError()` and returning typed Error arms exactly as before. The dispatcher derives `TOOLS_WITH_ERROR_ARM` from the bundled schema cache at server build, then mirrors `adcp_error` ↔ `errors[]` on the failure path of every tool in the set. Idempotent on already-two-layer responses (no duplicate or replacement). `update_content_standards` is the one tool whose Error arm requires a `success: false` discriminator beyond `errors[]`; the dispatcher stamps the constant when synthesising so the payload satisfies its `oneOf`. Schema-driven regression tests (43 cases) compile every Error-arm tool's response schema with Ajv and assert the dispatcher's emitted response validates on both Path A (`adcpError()` → envelope-only handler → payload synthesised) and Path B (typed `{errors:[...]}` arm handler → envelope synthesised), plus idempotency, non-Error-arm passthrough, and success-path untouched. Closes #1606. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Option C from RFC #1608 (issue #1606): the framework dispatcher auto-wraps the payload-layer
errors: [{code, message}, ...]array alongside the existing envelope-layer{adcp_error: {...}}block on the failure path of every tool whose response schema declares a typed Error arm.Adopters keep calling
adcpError()and returning typed{errors: [...]}arms exactly as before — no API changes. The wire change is on the failure path only and is strictly additive: responses that previously failed schema validation against the*Errorarm of theironeOfnow pass it.The set of affected tools is derived dynamically from the bundled schema cache at server build, so future AdCP minors that add Error-arm tools join automatically.
Background
error-code.json#GOVERNANCE_DENIED(and#GOVERNANCE_UNAVAILABLE) requires both layers on tasks whose response defines an Error arm but no structured rejection arm:The SDK has been emitting wire-incorrect (envelope-only or payload-only) error responses since
adcpError()was introduced. Storyboard chaining gaps masked the issue until 3.0.7 fixedget_products_refinechaining (see RFC § 1.3) — the SDK is the bug; the storyboard is doing its job surfacing drift.Tools auto-wrapped (18)
Derived from
schemas/cache/3.0.7/bundled/**/*-response.jsonwhose top-leveloneOfdeclares an arm withrequired: ["errors"]:create_media_buyupdate_media_buyprovide_performance_feedbackbuild_creativesync_audiencessync_catalogssync_event_sourceslog_eventactivate_signalsync_creativesget_creative_featuresvalidate_content_deliverylist_content_standardsget_media_buy_artifactsget_content_standardscreate_content_standardsupdate_content_standardscalibrate_contentImplementation
src/lib/server/error-arm-tools.ts(new): builds the per-tool descriptor map (tool name → ErrorArmDescriptor) lazily from the bundled schema cache, memoised per bundle key. Captures anyoneOf-discriminator constants the error arm requires beyonderrors[]itself (update_content_standardsdeclaressuccess: { const: false }).src/lib/server/create-adcp-server.ts: addsenrichErrorTwoLayer()and theapplyArmDiscriminatorshelper, called from the dispatcher'sfinalize()seam right aftersanitizeAdcpErrorEnvelope(). Uses the spread-then-override pattern:sc.errors = [projectEnvelopeToPayloadError(env)](Path A) andsc.adcp_error = projectPayloadErrorToEnvelope(first)(Path B). L2 JSON text fallback is mirrored viasyncContentJsonText().src/lib/server/errors.ts: docstring update to flag that the framework adds the payload layer for Error-arm tools. TheadcpError()API is unchanged.Idempotency policy: passthrough
Adopters who already emit a fully-formed two-layer response (envelope AND payload) pass through unchanged. The dispatcher detects existing layers via presence checks on both
sc.adcp_errorandsc.errors. Discriminator constants are stamped only when the framework synthesises the arm — not on already-two-layer responses, so an adopter's deliberate value is preserved.No env-var bypass
Per
feedback_wire_honestyandfeedback_node_env_allowlist, no NODE_ENV / env-var escape hatch is wired in. The wrap is unconditional on tools in the gate set.comply_test_controlleris not in the wrap setThe test controller is framework-internal and not part of the bundled AdCP schema cache. The gate's schema-derivation never picks it up. Locked in by an explicit assertion in the regression suite.
Test coverage
test/server-error-arm-two-layer.test.js— schema-driven regression suite (43 tests, all passing):adcpError()→ assert both layers present, fields project correctly, response satisfies the bundled response schema via Ajv, and L2 text mirrors structuredContent.{errors:[...]}arm directly → assert envelope synthesised, fields project, schema validates.get_productsandget_signalsfailure paths stay envelope-only.comply_test_controllerexclusion (1 test): not in the wrap set.adcp_error/errorslayers.Full SDK suite: 8236/8236 tests pass after the change.
Verification of the fix
test/examples/hello-seller-adapter-proposal-mode.test.js(the proposal-mode adapter that surfaced this via fix(adapter): hello_seller_adapter_proposal_mode create_media_buy fails schema validation against AdCP 3.0.7 #1600/fix(storyboard): proposal-mode create_media_buy request shape (closes #1600) #1603/fix(storyboard-runner): preserve sample_request fields in enrichers (#1604) #1607) passes.Migration recipe
docs/migration-6.14-to-6.15.md. One-line summary: no adopter code changes required; integration tests that snapshot error-response shape on the 18 tools may need a new top-levelerrors[]key.Test plan
get_products,get_signals) stay envelope-onlyupdate_content_standardsdiscriminator stamping verifiednpm run format:checkcleanRefs
docs/proposals/adcperror-two-layer-emission.md) — can be closed once this lands; the design is implemented.🤖 Generated with Claude Code