Module adcp.validation.client_hooks
Client-side hooks that run the schema validator around every AdCP tool call. Pre-send validation blocks malformed requests; post-receive validation catches field-name drift from agents (issue #249).
Global variables
var SERVER_DEFAULT_VALIDATION : ValidationHookConfig-
Server-side default — strict on both request and response sides. Used by :func:
serve()and the underlyingcreate_*_serverfactories when the adopter does not passvalidation=explicitly. Strict-by-default makes the SDK enforce wire conformance: a malformed request fails before the handler runs (VALIDATION_ERROR); a spec-divergent response fails after the handler returns. Catches the class of bug thatextra="allow"Pydantic models silently swallow (e.g. thepricing_optionsregression). Adopters opt out viaValidationHookConfig(responses="warn")(warn-only) orvalidation=None(off entirely).
Functions
def resolve_validation_modes(config: ValidationHookConfig | None = None) ‑> tuple[typing.Literal['strict', 'warn', 'off'], typing.Literal['strict', 'warn', 'off']]-
Expand source code
def resolve_validation_modes( config: ValidationHookConfig | None = None, ) -> tuple[ValidationMode, ValidationMode]: """Return the effective ``(requests, responses)`` modes. Resolution order (per side): 1. Explicit ``config.requests`` / ``config.responses`` (when set). 2. ``ADCP_VALIDATION_MODE`` env var — applies to both sides. 3. ``ADCP_ENV=prod|production`` flips the response default to ``warn``; requests fall back to ``warn`` (the type default). 4. Hard defaults: ``requests="warn"``, ``responses="strict"``. Read at call time (not import time) so tests that mutate env vars via ``patch.dict`` work without a module-level reset hook. """ explicit_req = config.requests if config is not None else None explicit_resp = config.responses if config is not None else None env_mode = _env_validation_mode() req: ValidationMode = explicit_req or env_mode or "warn" resp: ValidationMode = explicit_resp or env_mode or _default_response_mode() return req, respReturn the effective
(requests, responses)modes.Resolution order (per side):
- Explicit
config.requests/config.responses(when set). ADCP_VALIDATION_MODEenv var — applies to both sides.ADCP_ENV=prod|productionflips the response default towarn; requests fall back towarn(the type default).- Hard defaults:
requests="warn",responses="strict".
Read at call time (not import time) so tests that mutate env vars via
patch.dictwork without a module-level reset hook. - Explicit
def validate_incoming_response(tool_name: str,
data: Any,
mode: ValidationMode,
debug_logs: list[DebugLogEntry] | None = None) ‑> ValidationOutcome-
Expand source code
def validate_incoming_response( tool_name: str, data: Any, mode: ValidationMode, debug_logs: list[DebugLogEntry] | None = None, ) -> ValidationOutcome: """Run response validation per the configured mode. * ``off`` — no-op (returns a valid skipped outcome). * ``warn`` — log + return the invalid outcome so the caller can surface details without failing the task. * ``strict`` — return the invalid outcome so the caller fails the task. Never raises — matches the existing Python response contract where a validation failure turns a task into ``status=FAILED`` rather than raising out of the adapter. """ if mode == "off": return ValidationOutcome(valid=True, issues=[], variant="skipped") outcome = validate_response(tool_name, data) if not outcome.valid and mode == "warn": _log_warning(debug_logs, tool_name, "response", outcome) return outcomeRun response validation per the configured mode.
off— no-op (returns a valid skipped outcome).warn— log + return the invalid outcome so the caller can surface details without failing the task.strict— return the invalid outcome so the caller fails the task.
Never raises — matches the existing Python response contract where a validation failure turns a task into
status=FAILEDrather than raising out of the adapter. def validate_outgoing_request(tool_name: str,
params: Any,
mode: ValidationMode,
debug_logs: list[DebugLogEntry] | None = None) ‑> ValidationOutcome | None-
Expand source code
def validate_outgoing_request( tool_name: str, params: Any, mode: ValidationMode, debug_logs: list[DebugLogEntry] | None = None, ) -> ValidationOutcome | None: """Run request validation per the configured mode. * ``off`` — no-op (returns ``None``; validator is not consulted). * ``warn`` — log + continue; returns the outcome. * ``strict`` — raise :class:`SchemaValidationError` on failure. """ if mode == "off": return None outcome = validate_request(tool_name, params) if outcome.valid: return outcome if mode == "warn": _log_warning(debug_logs, tool_name, "request", outcome) return outcome raise build_validation_error(tool_name, "request", outcome.issues)Run request validation per the configured mode.
off— no-op (returnsNone; validator is not consulted).warn— log + continue; returns the outcome.strict— raise :class:SchemaValidationErroron failure.
Classes
class DebugLogEntry (*args, **kwargs)-
Expand source code
class DebugLogEntry(TypedDict, total=False): """Append-only entry shape for the ``debug_logs`` list threaded by the client and server call paths. ``total=False`` so callers can still construct partial entries.""" type: str message: str timestamp: str schema_variant: str issues: list[dict[str, Any]]Append-only entry shape for the
debug_logslist threaded by the client and server call paths.total=Falseso callers can still construct partial entries.Ancestors
- builtins.dict
Class variables
var issues : list[dict[str, typing.Any]]var message : strvar schema_variant : strvar timestamp : strvar type : str
class UnknownFieldPolicy (*args, **kwds)-
Expand source code
class UnknownFieldPolicy(str, Enum): """Server-side policy for unknown top-level tool arguments. Runs at the transport boundary before Pydantic request-model coercion can silently accept or drop extra fields. """ REJECT = "reject" STRIP = "strip" IGNORE = "ignore"Server-side policy for unknown top-level tool arguments.
Runs at the transport boundary before Pydantic request-model coercion can silently accept or drop extra fields.
Ancestors
- builtins.str
- enum.Enum
Class variables
var IGNOREvar REJECTvar STRIP
class ValidationHookConfig (requests: ValidationMode | None = None,
responses: ValidationMode | None = None,
unknown_fields: "UnknownFieldPolicy | Literal['reject', 'strip', 'ignore'] | None" = None)-
Expand source code
@dataclass(frozen=True) class ValidationHookConfig: """Per-side client validation modes. Defaults match the TS port (adcontextprotocol/adcp-client#694): * ``requests``: ``"warn"`` — strict would break callers that intentionally send partial payloads (error-path tests, exploratory probes). Storyboards and compliance runners that want hard-stop enforcement pass ``requests="strict"`` explicitly. * ``responses``: ``"strict"`` in dev/test, ``"warn"`` when ``ADCP_ENV`` is set to ``production`` / ``prod``. Strict-by-default makes the SDK a compliance harness: drift from an agent fails the task on the first call, not the Nth storyboard run. Resolution order for both sides at call time: 1. Explicit value on this config (``requests=`` / ``responses=``). 2. ``ADCP_VALIDATION_MODE`` env var (``strict`` / ``warn`` / ``off``) — applies to both sides unless overridden by an explicit value. Matches the TS port (adcontextprotocol/adcp-client). 3. ``ADCP_ENV=prod|production`` flips the response default to ``warn``; requests fall back to the type default. 4. Defaults: ``requests="warn"``, ``responses="strict"``. Only ``ADCP_ENV`` and ``ADCP_VALIDATION_MODE`` are consulted — generic ``ENV`` / ``ENVIRONMENT`` would collide with unrelated tooling (rails, postgres, 12-factor) and silently flip the SDK's default. """ requests: ValidationMode | None = None responses: ValidationMode | None = None #: Server-side policy for unsupported top-level tool arguments. #: ``None`` preserves existing permissive behavior. unknown_fields: UnknownFieldPolicy | Literal["reject", "strip", "ignore"] | None = NonePer-side client validation modes.
Defaults match the TS port (adcontextprotocol/adcp-client#694):
requests:"warn"— strict would break callers that intentionally send partial payloads (error-path tests, exploratory probes). Storyboards and compliance runners that want hard-stop enforcement passrequests="strict"explicitly.responses:"strict"in dev/test,"warn"whenADCP_ENVis set toproduction/prod. Strict-by-default makes the SDK a compliance harness: drift from an agent fails the task on the first call, not the Nth storyboard run.
Resolution order for both sides at call time:
- Explicit value on this config (
requests=/responses=). ADCP_VALIDATION_MODEenv var (strict/warn/off) — applies to both sides unless overridden by an explicit value. Matches the TS port (adcontextprotocol/adcp-client).ADCP_ENV=prod|productionflips the response default towarn; requests fall back to the type default.- Defaults:
requests="warn",responses="strict".
Only
ADCP_ENVandADCP_VALIDATION_MODEare consulted — genericENV/ENVIRONMENTwould collide with unrelated tooling (rails, postgres, 12-factor) and silently flip the SDK's default.Instance variables
var requests : Literal['strict', 'warn', 'off'] | Nonevar responses : Literal['strict', 'warn', 'off'] | Nonevar unknown_fields : UnknownFieldPolicy | Literal['reject', 'strip', 'ignore'] | None-
Server-side policy for unsupported top-level tool arguments.
Nonepreserves existing permissive behavior.