Skip to content

feat(server): strict response validation default in dev/test (#727)#757

Merged
bokelley merged 4 commits into
mainfrom
bokelley/issue-triage
Apr 22, 2026
Merged

feat(server): strict response validation default in dev/test (#727)#757
bokelley merged 4 commits into
mainfrom
bokelley/issue-triage

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • createAdcpServer({ validation: { responses } }) now defaults to 'strict' when NODE_ENV !== 'production' (was 'warn'). Handler drift fails with VALIDATION_ERROR carrying the field pointer instead of silently logging a warning.
  • Production default is unchanged: 'off'. Explicit per-call config still wins.
  • Three internal test files opt out (validation: { responses: 'off' }) because they use deliberately sparse fixtures to exercise envelope/state-store/idempotency middleware — schema conformance isn't their unit under test. test/lib/schema-validation-server.test.js continues to cover the validator itself.

Why

The compliance:skill-matrix harness keeps surfacing SERVICE_UNAVAILABLE from agents that returned spec-incomplete responses (see the test-agent-team review on #725). The dispatcher's response validator is the right layer — it catches drift once for every tool with a clear field pointer. Making it strict by default in dev/test catches drift while the handler is being written instead of in a downstream consumer.

Closes #727(A). Part (B) (tightening generated discriminated unions in the types generator) remains open.

Test plan

  • npm test — 5135 pass / 0 fail (baseline before flip: same count)
  • test/lib/schema-validation-server.test.js still asserts the three modes (strict / warn / off) behave as documented
  • Post-merge: rerun npm run compliance:skill-matrix and confirm any residual drift surfaces as a single validator error per tool (the measurement the issue called for)

🤖 Generated with Claude Code

createAdcpServer defaults responses: 'strict' when NODE_ENV !== 'production'
(was 'warn'), turning handler-returned schema drift into VALIDATION_ERROR
with the offending field path. Production default stays 'off'.

Opts out in three internal test files that use deliberately sparse
handler fixtures for middleware coverage; schema-validation-server.test.js
still exercises the validator itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
code-reviewer:
- clarify JSDoc that 'warn' still logs; 'strict' promotes to VALIDATION_ERROR
- idempotency.release() catch now logs (parity with post-handler path)
- test helper shallow-merges `validation` so a per-test override on one
  side doesn't silently clear the file-level opt-out on the other
- add dispatcher-level tests asserting the NODE_ENV-driven default flip
- changeset calls out NODE_ENV=undefined landing in the dev/test bucket

security-reviewer:
- gate `VALIDATION_ERROR` `details.issues[].schemaPath` behind
  `exposeErrorDetails` (same policy as SERVICE_UNAVAILABLE.details.reason);
  prod responses no longer fingerprint the handler's internal `oneOf`
  branch selection. Buyers keep pointer/message/keyword — enough to fix
  a drifted payload without revealing AJV branch state
- replay-loop finding filed as #758 (pre-existing; affects warn mode too,
  deserves its own circuit-breaker-vs-cache design)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley

Copy link
Copy Markdown
Contributor Author

Rebased on main and addressed expert review feedback in 6dfcb53.

code-reviewer (PR doc + correctness):

  • JSDoc clarified that warn still logs; strict additionally promotes to VALIDATION_ERROR
  • idempotency.release() catch now logs via logger.warn (parity with the sibling post-handler path at :1938)
  • Three test helpers now shallow-merge validation so a per-test override on one side doesn't silently clear the other
  • Added four dispatcher-level canary tests in test/lib/schema-validation-server.test.js asserting the NODE_ENV-driven default flip and the schemaPath gating behavior
  • Changeset now calls out NODE_ENV=undefined explicitly (Node's test runner doesn't set it)

security-reviewer (details.issues[].schemaPath leak):

  • details.issues[].schemaPath is now gated behind exposeErrorDetails (same policy as SERVICE_UNAVAILABLE.details.reason). Production responses no longer leak #/oneOf/<n>/properties/... that fingerprints the handler's internal branch selection. Buyers still get pointer, message, keyword.
  • M-2 replay-loop finding filed as idempotency: strict-mode VALIDATION_ERROR replay loop (unbounded re-execution) #758 (pre-existing, affects warn mode users too — deserves its own circuit-breaker-vs-cache design).

debugger passed the PR clean — no regressions, no timing-sensitive interactions with NODE_ENV capture.

npm test local: 5158 pass / 0 fail after rebase + fixes.

@bokelley bokelley merged commit 86ccc99 into main Apr 22, 2026
9 checks passed
bokelley added a commit that referenced this pull request Apr 22, 2026
…slim on creative-agent (#762)

* docs: surface response contracts per tool in docs/llms.txt

Each tool section now emits a "Response (success branch)" block with the
required + optional fields, mirroring the existing request block. Pulled
straight from the bundled JSON schemas; zero new source of truth.

Closes the drift path matrix runs kept surfacing — agents dropped
required response fields (missing format_id on creative_manifest, plural
hallucinations like creative_deliveries for creatives, missing top-level
currency) because the skill prose documented intent but the full
per-field contract lived in the generated schemas and was never indexed
in the llms.txt file Claude actually reads.

Anchored sections let skills point at docs/llms.txt#<tool> instead of
embedding the contract inline — keeps skills lean.

* docs(skills): point at llms.txt for field contracts; slim creative-agent

Every build-*-agent skill's "Tools and Required Response Shapes"
section now opens with a canonical-contracts pointer directing Claude
at the anchored llms.txt section for per-tool request + response
fields — one source of truth, refreshed on every schema sync.

build-creative-agent gets a surgical slim: the verbose response-shape
blocks (which duplicate llms.txt verbatim now that #761 landed) are
collapsed into a compact handler-binding table. Gotchas and anti-
patterns stay; field enumerations move to llms.txt. Net -94 lines,
same signal. The other 7 skills get the pointer line only — follow-up
passes can do per-skill surgical slims once the pattern is validated.

With strict response validation default in dev (#727/#757), drift
between a skill example and the schema now fails immediately at the
call site with the exact field path. The skill's job is to document
patterns/gotchas; the schema's job is to enforce shape. This
separation was muddied when every skill tried to re-state the shape
inline.

* docs(skills): imperative callout + anchor column per expert review

Experts flagged the passive "contracts live at X" wording — Claude
doesn't follow pointers without an imperative trigger. Rewrote to lead
with "Before writing any handler's return statement, fetch
docs/llms.txt and grep for #### \`<tool_name>\`" and reframed the
validation safety-net from threat to permission. Applied to all 8
skills.

Added a per-row Contract column to the creative-agent handler-binding
table with direct anchor links — more reliably followed than a general
pointer three lines up.

Regenerated schemas/types (upstream context-object shift) and widened
si-session-manager.ts generateInitialResponse signature from string to
unknown so the overrides accept the new ContextObject shape. Unblocks
CI's Validate generated files check.
@github-actions github-actions Bot mentioned this pull request Apr 22, 2026
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.

Tighten generated types + default response validation to catch schema drift at the right layer

1 participant