This repository was archived by the owner on Sep 11, 2026. It is now read-only.
Repository navigation
feat(sdk): enforce WellKnownManifest.ver in the endpoint resolvers - #48
Merged
Merged
Conversation
…nd give the manifest version its own constant The manifest's ver has stated a consumer-side rule since the manifest was unified, and no SDK applied it: the Go, Python and TypeScript endpoint resolvers decoded the endpoint and dropped ver unread, so a manifest at any version, or with no version at all, was accepted. The reference implementation's schema validator was the only enforcement anywhere, and it enforced the wrong shape — an exact match on "1.0", which refuses the additive minor revisions the rule exists to admit. The rule is now stated in full on the field and enforced where the document is read. A consumer reads ver before any other member, accepts a recognised MAJOR whatever the MINOR, and refuses an unrecognised MAJOR, a value that is not MAJOR.MINOR, and an absent ver. Absent is refused rather than tolerated: the field is required by the wire shape, the document sits at a fixed unversioned path, and it is read before any signature is checked, so a layout the reader cannot classify must not supply the endpoint a signed call is then sent to. The gate runs on the endpoint face only — the well-known key resolver reads a JWKS document that carries no manifest version — and a refusal is a verdict: never cached, and classified not-sent by the client tier like the other resolver verdicts. The value gets one owner. helpers.WellKnownManifestVersion (WellKnownManifestVersion in Python and TypeScript) is pinned to the wire-constants corpus beside ProtocolVersion and deliberately not derived from it. The two are separate namespaces and the coupling runs one way: a manifest layout change bumps both numbers, a protocol change that leaves the manifest untouched bumps only ProtocolVersion, so a reader that parses only manifests upgrades when the manifest changes and at no other time. The proto header and the manifest field now say so. Three guards hold the three halves together. A new corpus, manifest-version-vectors.json, is emitted from the real Go rule and replayed by all three languages, through the pure check and through the real endpoint resolver against a stub origin. A new conformance guard pins the field comment to every clause the corpus exercises, in both directions. And the existing ver contract guard now reads the manifest's expected value from the WellKnownManifestVersion vector entry rather than from ProtocolVersion, so the guard itself no longer couples the two namespaces. Every fixture that serves a manifest to the real resolver now carries ver, which is the compatibility note for the change: a manifest served without ver was accepted before and is refused now. A conforming producer already stamps it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUwf5FonFW8pAjkyD5raNu
…a verdict, and the key-face exemption pinned by a test Two review rounds of the manifest-version gate found the same shape three times: a decision recorded as prose where a test should have held it. The key-face exemption was one. The endpoint resolvers gate on ver and the well-known key resolvers do not, because a key document is a plain JWK Set that carries no manifest version — but no test in any language served a key document with a ver the endpoint face would refuse and checked that keys still resolve, so moving the gate into the shared fetch passed every test. Each language now has that test. The comments that still described the pre-branch shape are corrected with it: wellKnownDoc no longer claims a key set at proto field 5 (there is none) or one body serving both faces, and the key resolver no longer says it reads ramp.json keys. A non-string ver was the second. Go decoded ver into a string, so a document carrying `"ver": 1` failed the decode and surfaced as a transport error the client tier retried; Python and TypeScript treat any non-string as absent and return the final verdict. Go now holds ver raw and reads it through manifestVer, which yields the absent value for anything that is not a JSON string, so the three languages return the same verdict and the client tier classifies it not-sent everywhere. A resolver-level test in each language serves a number, a null and an object and asserts the verdict — and, in Go, that it is reported as absent rather than spelled from the raw bytes. The not-sent classification in Python and TypeScript was the third: Go had a taxonomy test for the new verdict and the two ports did not, so reverting either arm left every test green. Both now have one, in the shape of the existing injected-resolver tests beside them. Go declared the sentinel twice — helpers.ErrManifestVersionRefused and a separate resolvers.ErrManifestVersionRefused, joined by a double wrap — and the client route matched only the second, so an injected resolver wrapping the helper sentinel was retried forever. The resolvers name is now a re-export of the helper sentinel, the shape ErrUnknownKey already uses, and the wrap is single. Both symbol-map rows stay, as they do for ErrUnknownKey. Smaller things the same rounds caught. The refused ver is clipped to 64 characters before it is echoed into the error message, in all three languages: the body is read up to 1 MiB and a refusal is never cached, so an unclipped echo let a hostile origin size every error. The rationale paragraph that had been copied into seven source sites now lives on WellKnownManifest.ver in the proto, and each SDK site says only what it does and points there. The "read the same by coincidence, not by rule" sentence beside the one-way coupling rule is reworded so the two no longer read as a contradiction. A stale Python cross-reference, a "two sentinels" count in the Go README, and "seven wire constants" in two replay tests are corrected. The conformance ver-contract guard reads the wire-constants vector once through one parameterised lookup instead of two copies of the reader, the Python and TypeScript manifest builders take a ver parameter so the parity tests no longer hand-build the document, and a tautological isinstance assertion after a pytest.raises is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mc78kH8LccesLjakv77nmK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
WellKnownManifest.verhas stated a consumer-side rule since the manifest was unified, and no SDK applied it: the Go, Python and TypeScript endpoint resolvers droppedverunread, so a manifest at any version, or with none, was accepted.This PR states the rule in full and enforces it on the endpoint face in all three languages:
verbefore any other member. Accept a recognised MAJOR whatever the MINOR. Refuse an unrecognised MAJOR, a value that is notMAJOR.MINOR, and an absentver. A refusal is a verdict: never cached, classified not-sent by the client tier.helpers.WellKnownManifestVersion(WellKnownManifestVersionin Python/TS), pinned to the wire-constants corpus besideProtocolVersionand deliberately not derived from it. The coupling runs one way: a manifest layout change bumps both numbers, a protocol change alone bumps onlyProtocolVersion.manifest-version-vectors.jsonreplayed by all three languages (pure rule + real resolver against a stub origin). Newconformance/manifest_version_rule_test.gopins the proto comment to the corpus in both directions.ver_field_contract_test.gonow reads the manifest's expected value from its own vector entry, so the guard stops coupling the two namespaces.proto-ramp.mdx, both changelogs.Compatibility
A manifest served without
verwas accepted before and is refused now. The field has been required by the wire shape since the unified manifest was introduced, so a conforming producer already stamps it. No wire change.Verification
scripts/ci-local.shgreen (buf 1.66.1, the pinned version; 1.71.0 produces a differentdescriptor.binpb).sdk/python/tests: 1634 passed.sdk/ts: tsc strict + 1535 vitest passed. Website builds.Please merge with a merge commit rather than a squash: the reference implementation is being re-pinned to
ad9d688and that SHA must stay reachable.🤖 Generated with Claude Code
https://claude.ai/code/session_01UUwf5FonFW8pAjkyD5raNu