Skip to content

feat(validation): schema-driven validation with client hooks and opt-in server middleware#694

Merged
bokelley merged 7 commits into
mainfrom
bokelley/issue-688
Apr 21, 2026
Merged

feat(validation): schema-driven validation with client hooks and opt-in server middleware#694
bokelley merged 7 commits into
mainfrom
bokelley/issue-688

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

  • Add AJV-based validation against the bundled AdCP JSON schemas so field-name drift gets caught at the SDK layer instead of at storyboard-run time (closes Schema-driven validation: client hooks + opt-in server middleware #688).
  • Client hooks: pre-send request validation + post-receive response validation on TaskExecutor, configurable per side (strict / warn / off). off short-circuits before AJV runs for perf-sensitive prod.
  • Server middleware: opt-in on createAdcpServer — strict returns adcpError('VALIDATION_ERROR') with every issue carried under details.issues (JSON Pointer + keyword + schemaPath).
  • Async response variants (-submitted, -working, -input-required) selected by payload status shape, not just task name.
  • Build step copies schemas/cache/<ver>/{bundled,core,…}dist/lib/schemas-data/<ver>/ so the validator reads schemas from the installed package.

Defaults

  • requests: 'warn' everywhere — strict-by-default would break callers that intentionally send partial payloads (error-path tests, exploratory probes). Storyboards and third-party clients set strict explicitly.
  • responses: 'strict' in dev/test, 'warn' in production — preserves existing strictSchemaValidation contract. Legacy strictSchemaValidation: false still maps to response warn.

Test plan

  • 21 unit tests in test/lib/schema-validation.test.js exercising the core validator (variant selection, JSON Pointer accuracy, additionalProperties tolerance, mode defaults)
  • 7 integration tests in test/lib/schema-validation-server.test.js for the server middleware via dispatchTestRequest
  • Full lib regression: 3910 / 3910 pass

🤖 Generated with Claude Code

bokelley and others added 7 commits April 21, 2026 06:57
…in server middleware

Adds JSON-schema validation (AJV) against the bundled AdCP schemas on both
sides of the wire, so field-name drift is caught at the SDK layer instead
of at storyboard-run time. Closes #688.

- Core validator loads `bundled/` sync + flat-tree async variants, selects
  the response variant by `status` shape, emits structured `VALIDATION_ERROR`
  with JSON Pointer + schema path
- Client: `validation.requests` / `validation.responses` on SingleAgentClient,
  default request=warn / response=strict-in-dev, `off` short-circuits
- Server: opt-in `validation` on createAdcpServer; strict returns
  `adcpError('VALIDATION_ERROR')` with `details.issues` carrying every pointer
- Build: copies `schemas/cache/<ver>/{bundled,core,...}` → `dist/lib/schemas-data/`
- 28 new tests, all 3910 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's code-quality integrity job runs `npm clean && build:lib` without first
running `sync-schemas`, so the cache at schemas/cache/<ver>/ isn't present.
Skip the copy with a warning so the build still succeeds — the runtime
loader falls back to the same source path and tests that need the schemas
run from the Test & Build job where sync-schemas is wired in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream sales-catalog-driven storyboard adds an expect_substitution_safe
step (runner inspects the preview artifact from a prior step). It is a
harness assertion, not an AdCP tool, so it belongs alongside the
expect_webhook* pseudo-tasks in HARNESS_TASKS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolved package.json/package-lock.json conflicts (both branches added
to dependencies — kept ajv + ajv-formats alongside fast-check).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley merged commit 77ea1b9 into main Apr 21, 2026
14 checks passed
bokelley added a commit that referenced this pull request Apr 21, 2026
Resolves two conflicts in generated files (core.generated.ts,
schemas.generated.ts) by regenerating after `sync-schemas`.

Also aligns the conformance schema loader with main's new
`copy-schemas-to-dist.ts` pattern from #694: prefer
`dist/lib/schemas-data/<ver>/bundled/` (staged at build time) and fall
back to `schemas/cache/<ver>/bundled/` for dev. Removes the now-
redundant `schemas/cache/latest/bundled/**/*.json` entry from the
package's `files` field — the copy script ships them through
`dist/lib/**` instead.

`detectSchemaVersion()` now reads the canonical `ADCP_VERSION` constant
from `src/lib/version.ts` (kept in sync by `sync-version`) rather than
reading the on-disk `ADCP_VERSION` file at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Apr 22, 2026
Flip createAdcpServer's request validation default from 'off' to 'warn'
when NODE_ENV !== 'production', mirroring the asymmetric default already
in place for responses ('strict' in dev/test, 'off' in production).

Production behaviour is unchanged: the default stays 'off' when
NODE_ENV === 'production', so prod request paths pay no AJV cost.

Warn mode logs a single Schema validation warning (request) line per
mismatched payload through the configured logger and still dispatches
to the handler — nothing is rejected. Keeps the two validation sides
symmetric and surfaces upstream schema tightenings (e.g. adcp#2795's
required asset_type discriminator) as diagnostics during handler
development instead of as a downstream VALIDATION_ERROR after deploy.

Opt out via validation: { requests: 'off' } or NODE_ENV=production.

Refs #694 (original intent for requests: 'warn'), #727 A (response-side
default precedent).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot mentioned this pull request Apr 22, 2026
KonstantinMirin pushed a commit to KonstantinMirin/adcp-client-python that referenced this pull request May 13, 2026
…in server middleware

Port the TS SDK's schema validator (adcontextprotocol/adcp-client#694) so
field-name drift is caught at the SDK layer instead of at storyboard-run
time (closes adcontextprotocol#249).

- Bundled AdCP JSON schemas drive validation via jsonschema; compiled
  validators cached by (tool, direction) and resolved against the
  packaged ``_schemas/`` tree or the repo-relative ``schemas/cache/``.
- Client hooks: pre-send request validation + post-receive response
  validation on both MCP and A2A adapters, configurable per side
  (strict / warn / off). Threaded through ``ADCPClient(validation=...)``.
- Server middleware: opt-in on ``create_tool_caller`` /
  ``create_mcp_tools`` — strict raises ``ADCPTaskError(VALIDATION_ERROR)``
  with every issue under ``details.issues`` (JSON Pointer + keyword +
  schema path).
- Async variants (``-submitted``, ``-working``, ``-input-required``)
  selected by payload ``status`` shape, not tool name alone.
- ``scripts/bundle_schemas.py`` mirrors ``schemas/cache/`` →
  ``src/adcp/_schemas/`` before wheel build; wired into
  ``make regenerate-schemas``, ``make build``, and CI.

Defaults diverge from the TS port: Python responses default to ``warn``
(not ``strict``) because the SDK had no pre-existing strict response
validator to preserve — breaking existing tests that stub non-spec
payloads would be too noisy. Set ``ADCP_ENV=dev`` or pass
``ValidationHookConfig(responses="strict")`` to opt in.

Co-Authored-By: Claude Opus 4.7 (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.

Schema-driven validation: client hooks + opt-in server middleware

1 participant