You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
enumMetadata.recovery for the signing family is unreachable, and the SDK hand-transcribes a partial private copy of the block the schema says SDKs MUST consume #1400
adcp/_schemas/3.2/enums/request-signing-error-code.json carries 29 enum members and 30 enumMetadata rows, 29 of which carry recovery. Its own $comment is normative and addressed at SDKs:
Normative recovery classification for request-signing transport errors. SDKs MUST consume this block instead of parsing enumDescriptions or inferring from code names.correctable means the signer can change the request or credential before retrying; transient …
Instead, adcp/signing/errors.py:114 hand-transcribes a partial copy into a private constant:
#: Request-family codes whose ``enumMetadata.recovery`` is ``transient`` in the#: pinned ``request-signing-error-code.json``: the fetch did not complete, so the#: same input can succeed later. Every other code is terminal or correctable._TRANSIENT_CODES=frozenset({
REQUEST_SIGNATURE_CAPABILITIES_UNREACHABLE,
REQUEST_SIGNATURE_BRAND_JSON_UNREACHABLE,
REQUEST_SIGNATURE_JWKS_UNAVAILABLE,
})
and adcp/signing/agent_resolver.py:620 names the obligation in a comment while implementing it by hand:
The two codes differ in the enumMetadata.recovery classification the spec requires SDKs to consume, so this hop carries its own code explicitly
Three properties follow:
The copy is private, so an adopter cannot reach it.
The copy is partial — it is the transient set only. The terminal / correctable split, which is the distinction that decides whether a caller may retry at all, is not represented anywhere in the package.
It is hand-maintained against a shipped schema, so it can drift from the pin it quotes with nothing to catch it.
Why it matters
An adopter that needs the classification has two options, and both are the ones the spec text is written to prevent: read the private adcp._schemas package, or hand-write a 29-row table from the code names. We hand-wrote it, and our table disagrees with the pin on six codes — which is the exact drift a public accessor would have made impossible.
derived at import from _schemas/<pin>/enums/request-signing-error-code.json rather than transcribed, so _TRANSIENT_CODES becomes a view over it and cannot drift. adcp.types.Recovery already exists as the enum, so the return type needs nothing new.
Happy to send the PR if the shape is agreeable.
Context
#1151 did this for the standard error family ("align standard recovery defaults with published metadata"). The signing family is the same obligation, unaddressed, and its schema states the MUST explicitly.
Classification: Bug Bucket(s): signing (no signing label exists in this repo — gap noted) Status: ready-to-implement Milestone: omitted (no version target named)
What the experts said:
ad-tech-protocol-expert: confirmed spec violation — the enumMetadata.$comment MUST is real and latent; current _TRANSIENT_CODES values happen to match the schema today, but the hand-maintained copy will silently drift on any schema update; proposed schema-loader approach is correct and mirrors the accepted _build_standard_error_codes() pattern; one risk: the signing module is on the hot import path, so the loader must inherit the two-path lookup (adcp/_schemas/ first, schemas/cache/ fallback) already used by helpers.py
code-reviewer: confirmed the "six codes disagree" claim is not accurate against the current code (3 transient codes match exactly), but the structural problem is real — no public terminal/correctable classification, no schema-derived source, no drift guard; implementation is a direct adaptation of _build_standard_error_codes() at src/adcp/server/helpers.py:112; non-breaking on all three counts
My take: Both experts converge with no disagreement. The fix is clearly non-breaking — _TRANSIENT_CODES is private and the schema-derived result is identical to the current hardcoded set, so SignatureVerificationError.transient behavior is unchanged. The prior art (issue #1151 → PR #1156) establishes the exact pattern to follow.
Implementation scope:
src/adcp/signing/errors.py: Add _build_signing_recovery() that reads enumMetadata from schemas/cache/3.2/enums/request-signing-error-code.json using the same two-path lookup as _read_error_code_schema() in helpers.py (packaged adcp/_schemas/{bundle_key}/ first, source-tree fallback). Derive _TRANSIENT_CODES as frozenset(c for c, r in _SIGNING_RECOVERY.items() if r == "transient"). Export SIGNING_RECOVERY: Mapping[str, str] (all 29 codes), recovery_for(code: str) -> Literal["correctable", "transient", "terminal"], and the three typed frozensets SIGNING_TRANSIENT_CODES, SIGNING_TERMINAL_CODES, SIGNING_CORRECTABLE_CODES for symmetry with helpers.py.
src/adcp/signing/__init__.py: Re-export the new public names from the errors import block and add them to __all__.
tests/test_signing_error_recovery.py (new): Parametrized tests asserting recovery_for(code) matches enumMetadata for at least one code per category, and SIGNING_TRANSIENT_CODES == frozenset({<3 codes>}) against the pinned schema. Mirrors the conformance-test pattern in tests/conformance/.
Non-breaking rationale: _TRANSIENT_CODES is private; the schema-derived set is identical to the current hardcoded set; recovery_for/SIGNING_RECOVERY are new names.
Correcting one sentence in the issue body about the adopter side. The upstream finding is unchanged; my description of what the adopter does was wrong, and the accurate version makes the case stronger.
What I wrote: "This repo hand-writes it, and the hand table disagrees with the pin on six codes."
What the adopter actually does: it derives recovery from the pinned JSON wherever it can reach it. For all 92 standard error codes the loader reads the bundled schema out of the installed wheel and takes the value straight from enumMetadata:
It also derives the signing code members from the SDK rather than re-listing them — dict(adcp.signing.errors.REQUEST_TO_WEBHOOK_CODE), with a module comment explaining that a transcribed copy "would be a second source for a string the spec grades byte-for-byte, and it would drift the first time the SDK's taxonomy grew."
So there is exactly one place it hand-writes a recovery classification, and it is the signing family — because that family's enumMetadata ships in a different file (enums/request-signing-error-code.json) and nothing in the public API exposes it. The adopter is not declining to derive; it has nothing to derive from.
Why this strengthens the issue rather than softening it. An adopter that already reads enumMetadata out of the wheel for 92 codes, that already refuses to re-list the signing codes on drift grounds, and that already quotes the same MUST in its own module docstring, still ends up hand-maintaining 29 rows for one family. That is the cleanest possible demonstration that the gap is in the surface and not in adopter discipline — and it is the reason the divergence-on-six-codes appeared at all.
It also means the adopter has a local fix available without waiting for upstream: point the same loader at the second file. I will take that. The upstream ask stands on its own merits — the SDK quotes "SDKs MUST consume this block" in agent_resolver.py:620 while errors.py:116 implements it as a private _TRANSIENT_CODES frozenset holding 3 of the 29 rows, with the terminal/correctable split represented nowhere in the package. A consumer of the library should not have to read adcp._schemas to get it.
Fixed by #1403 (squash 86ae412): the signing family's normative enumMetadata.recovery classification is now published from the bundled schema instead of a hand-transcribed partial copy.
Activity
bokelley commentedon Oct 6, 2026
Triage
Classification: Bug
Bucket(s): signing (no
signinglabel exists in this repo — gap noted)Status: ready-to-implement
Milestone: omitted (no version target named)
What the experts said:
enumMetadata.$commentMUST is real and latent; current_TRANSIENT_CODESvalues happen to match the schema today, but the hand-maintained copy will silently drift on any schema update; proposed schema-loader approach is correct and mirrors the accepted_build_standard_error_codes()pattern; one risk: the signing module is on the hot import path, so the loader must inherit the two-path lookup (adcp/_schemas/first,schemas/cache/fallback) already used byhelpers.py_build_standard_error_codes()atsrc/adcp/server/helpers.py:112; non-breaking on all three countsMy take: Both experts converge with no disagreement. The fix is clearly non-breaking —
_TRANSIENT_CODESis private and the schema-derived result is identical to the current hardcoded set, soSignatureVerificationError.transientbehavior is unchanged. The prior art (issue #1151 → PR #1156) establishes the exact pattern to follow.Implementation scope:
src/adcp/signing/errors.py: Add_build_signing_recovery()that readsenumMetadatafromschemas/cache/3.2/enums/request-signing-error-code.jsonusing the same two-path lookup as_read_error_code_schema()inhelpers.py(packagedadcp/_schemas/{bundle_key}/first, source-tree fallback). Derive_TRANSIENT_CODESasfrozenset(c for c, r in _SIGNING_RECOVERY.items() if r == "transient"). ExportSIGNING_RECOVERY: Mapping[str, str](all 29 codes),recovery_for(code: str) -> Literal["correctable", "transient", "terminal"], and the three typed frozensetsSIGNING_TRANSIENT_CODES,SIGNING_TERMINAL_CODES,SIGNING_CORRECTABLE_CODESfor symmetry withhelpers.py.src/adcp/signing/__init__.py: Re-export the new public names from theerrorsimport block and add them to__all__.tests/test_signing_error_recovery.py(new): Parametrized tests assertingrecovery_for(code)matchesenumMetadatafor at least one code per category, andSIGNING_TRANSIENT_CODES == frozenset({<3 codes>})against the pinned schema. Mirrors the conformance-test pattern intests/conformance/._TRANSIENT_CODESis private; the schema-derived set is identical to the current hardcoded set;recovery_for/SIGNING_RECOVERYare new names.pytest tests/test_signing_error_recovery.py,mypy src/adcp/signing/errors.py,ruff check src/adcp/signing/.Triaged by Claude Code. Session: https://claude.ai/code/session_01Lcr87zMApJFw62yS3qAipn
Generated by Claude Code
KonstantinMirin commentedon Oct 6, 2026
Correcting one sentence in the issue body about the adopter side. The upstream finding is unchanged; my description of what the adopter does was wrong, and the accurate version makes the case stronger.
What I wrote: "This repo hand-writes it, and the hand table disagrees with the pin on six codes."
What the adopter actually does: it derives
recoveryfrom the pinned JSON wherever it can reach it. For all 92 standard error codes the loader reads the bundled schema out of the installed wheel and takes the value straight fromenumMetadata:It also derives the signing code members from the SDK rather than re-listing them —
dict(adcp.signing.errors.REQUEST_TO_WEBHOOK_CODE), with a module comment explaining that a transcribed copy "would be a second source for a string the spec grades byte-for-byte, and it would drift the first time the SDK's taxonomy grew."So there is exactly one place it hand-writes a recovery classification, and it is the signing family — because that family's
enumMetadataships in a different file (enums/request-signing-error-code.json) and nothing in the public API exposes it. The adopter is not declining to derive; it has nothing to derive from.Why this strengthens the issue rather than softening it. An adopter that already reads
enumMetadataout of the wheel for 92 codes, that already refuses to re-list the signing codes on drift grounds, and that already quotes the same MUST in its own module docstring, still ends up hand-maintaining 29 rows for one family. That is the cleanest possible demonstration that the gap is in the surface and not in adopter discipline — and it is the reason the divergence-on-six-codes appeared at all.It also means the adopter has a local fix available without waiting for upstream: point the same loader at the second file. I will take that. The upstream ask stands on its own merits — the SDK quotes "SDKs MUST consume this block" in
agent_resolver.py:620whileerrors.py:116implements it as a private_TRANSIENT_CODESfrozenset holding 3 of the 29 rows, with the terminal/correctable split represented nowhere in the package. A consumer of the library should not have to readadcp._schemasto get it.feat(signing): publish the recovery classification the signing schema…
bokelley commentedon Oct 6, 2026
Fixed by #1403 (squash 86ae412): the signing family's normative enumMetadata.recovery classification is now published from the bundled schema instead of a hand-transcribed partial copy.