Skip to content

fix(server): adcpError two-layer emission for AdCP error arms (#1606)#1610

Merged
bokelley merged 1 commit intomainfrom
bokelley/fix-1606-adcperror-two-layer
May 8, 2026
Merged

fix(server): adcpError two-layer emission for AdCP error arms (#1606)#1610
bokelley merged 1 commit intomainfrom
bokelley/fix-1606-adcperror-two-layer

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented May 8, 2026

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 *Error arm of their oneOf now 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 seller populates errors[].code: GOVERNANCE_DENIED in the payload AND adcp_error.code: GOVERNANCE_DENIED on the envelope per the two-layer model"

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 fixed get_products_refine chaining (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.json whose top-level oneOf declares an arm with required: ["errors"]:

Track Tool
media-buy create_media_buy
media-buy update_media_buy
media-buy provide_performance_feedback
media-buy build_creative
event-tracking sync_audiences
event-tracking sync_catalogs
event-tracking sync_event_sources
event-tracking log_event
signals activate_signal
creative sync_creatives
creative get_creative_features
content-standards validate_content_delivery
content-standards list_content_standards
content-standards get_media_buy_artifacts
content-standards get_content_standards
content-standards create_content_standards
content-standards update_content_standards
content-standards calibrate_content

Note during implementation the schema audit showed the RFC's table swapped two tools: preview_creative was listed but its errors[] requirement is nested inside oneOf[1].properties.results.items.oneOf[1] (per-item, not top-level), and update_content_standards was missing. The total count of 18 still matches; this PR's set is the corrected one. Suggest updating RFC #1608 to match.

Implementation

  • 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 any oneOf-discriminator constants the error arm requires beyond errors[] itself (update_content_standards declares success: { const: false }).
  • src/lib/server/create-adcp-server.ts: adds enrichErrorTwoLayer() and the applyArmDiscriminators helper, called from the dispatcher's finalize() seam right after sanitizeAdcpErrorEnvelope(). Uses the spread-then-override pattern: sc.errors = [projectEnvelopeToPayloadError(env)] (Path A) and sc.adcp_error = projectPayloadErrorToEnvelope(first) (Path B). L2 JSON text fallback is mirrored via syncContentJsonText().
  • src/lib/server/errors.ts: docstring update to flag that the framework adds the payload layer for Error-arm tools. The adcpError() 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_error and sc.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_honesty and feedback_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_controller is not in the wrap set

The 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):

  • Schema audit (1 test): asserts the bundled schemas yield exactly 18 Error-arm tools.
  • Coverage gate (1 test): asserts every tool in the set has a registration entry in the test (so a new Error-arm tool can't ship without a regression test).
  • Path A (18 tests, one per tool): adopter calls adcpError() → assert both layers present, fields project correctly, response satisfies the bundled response schema via Ajv, and L2 text mirrors structuredContent.
  • Path B (18 tests): adopter returns {errors:[...]} arm directly → assert envelope synthesised, fields project, schema validates.
  • Idempotency (1 test): handler emits both layers manually → no duplication or replacement.
  • Non-Error-arm tools (2 tests): get_products and get_signals failure paths stay envelope-only.
  • comply_test_controller exclusion (1 test): not in the wrap set.
  • Success path untouched (1 test): non-error responses on Error-arm tools carry no adcp_error/errors layers.

Full SDK suite: 8236/8236 tests pass after the change.

Verification of the fix

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-level errors[] key.

Test plan

  • Schema-driven regression suite covers all 18 tools on both paths
  • Idempotency policy locked in by test
  • Non-Error-arm tools (get_products, get_signals) stay envelope-only
  • update_content_standards discriminator stamping verified
  • Full SDK test suite passes (8236 tests)
  • Proposal-mode adapter test passes
  • npm run format:check clean
  • Changeset (minor bump) added

Refs

🤖 Generated with Claude Code

…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>
@bokelley bokelley merged commit 918385f into main May 8, 2026
10 checks passed
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