diff --git a/changelog.d/20261008_lab-8444.md b/changelog.d/20261008_lab-8444.md new file mode 100644 index 0000000..84fa045 --- /dev/null +++ b/changelog.d/20261008_lab-8444.md @@ -0,0 +1,15 @@ +### Conformance — cachekit-rs runs file-backend 1.2.0 and python-frame.json + +- [`conformance/sdks.json`](conformance/sdks.json) reads cachekit-rs at `b5fb2a6`, which vendors + `file-backend.json` `1.2.0` and, for the first time, `python-frame.json` byte for byte + ([cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158)). In + [`conformance/coverage.md`](conformance/coverage.md), cachekit-rs's `FILE-2`, `IOP-19` and + `IOP-20` move to `covered`, and `FILE-1`, `FILE-3`, `FILE-4`, `ENC-5`, `ENC-10` and `WIRE-21` to + `partial (gap)`. +- [Feature matrix](sdk-feature-matrix.md#protocol-compliance) **Test vectors in CI** row and note + ¹⁶ record the two copies for cachekit-rs, unreleased. The **Storage container** row and note ¹⁵ + record cachekit-rs's WIRE-21 violations: it decodes cachekit-ts's bare ByteStorage envelope in + every release and on `main`, and crates.io 0.10.0 decodes a CK frame in auto mode. On `main`, + on a client without encryption, cachekit-rs refuses every CK frame in `python-frame.json`. + WIRE-21's gap in [`conformance/requirements.json`](conformance/requirements.json) names the + bare-envelope decode. diff --git a/conformance/coverage.md b/conformance/coverage.md index b4a4779..745bb5a 100644 --- a/conformance/coverage.md +++ b/conformance/coverage.md @@ -14,7 +14,7 @@ How ids are assigned, and what each status means: [README.md](README.md#coverage | :--- | :--- | :--- | | cachekit-py | [`ddecec3`](https://github.com/cachekit-io/cachekit-py/commit/ddecec3f5897d387f194441446d7b085d688c40e) | `cache-keys.json` 1.1.0, `decode-bounds.json` 1.2.0, `file-backend.json` 1.2.0, `freshness-headers.json` 1.0.0, `interop-mode.json` 1.3.0, `path-encoding.json` 1.2.0, `python-frame.json` unversioned, `wire-format.json` 1.4.0 | | cachekit-ts | [`a8e8e0b`](https://github.com/cachekit-io/cachekit-ts/commit/a8e8e0b57573d759253a1f89ffa48ca12079719f) | `decode-bounds.json` 1.2.0, `encryption.json` 1.3.0, `file-backend.json` 1.1.0 (not the current revision), `freshness-headers.json` 1.0.0, `interop-mode.json` 1.3.0, `path-encoding.json` 1.2.0, `wire-format.json` 1.3.0 (not the current revision) | -| cachekit-rs | [`a7feb62`](https://github.com/cachekit-io/cachekit-rs/commit/a7feb62b5b589ba6126c20fa36b3f55bf02e82a2) | `decode-bounds.json` 1.2.0, `file-backend.json` 1.1.0 (not the current revision), `freshness-headers.json` 1.0.0, `interop-mode.json` 1.3.0, `path-encoding.json` 1.2.0 | +| cachekit-rs | [`b5fb2a6`](https://github.com/cachekit-io/cachekit-rs/commit/b5fb2a67392ce8dfd4dc199ad7ec7233a661d43b) | `decode-bounds.json` 1.2.0, `file-backend.json` 1.2.0, `freshness-headers.json` 1.0.0, `interop-mode.json` 1.3.0, `path-encoding.json` 1.2.0, `python-frame.json` unversioned | ## Summary @@ -32,7 +32,7 @@ How ids are assigned, and what each status means: [README.md](README.md#coverage | :--- | ---: | ---: | ---: | ---: | ---: | | cachekit-py | 8 | 80 | 9 | 73 | 47 | | cachekit-ts | 5 | 75 | 9 | 57 | 71 | -| cachekit-rs | 3 | 59 | 9 | 58 | 88 | +| cachekit-rs | 6 | 57 | 8 | 58 | 88 | Not indexed yet: [`spec/interop-v2.md`](../spec/interop-v2.md). @@ -66,21 +66,21 @@ Marked not-a-requirement in `spec/cache-key-format.md`: | [ENC-2](../spec/encryption.md#enc-2) | *AAD v0x03 Format*: … failure into a metadata-tamper oracle. Likewise, the post-decryption unenvelope step **MUST** be selected by the reader's configured serializer/mode — never by sniffing the decrypted bytes or falling back between … | **Gap:** Every encrypted vector decrypts to the container its reader is configured for, or to bare bytes that no container step reads, so a reader that picks the container by sniffing the plaintext, or falls back from one container to another, passes them all. | gap | gap | gap | | [ENC-3](../spec/encryption.md#enc-3) | *AAD v0x03 Format*: … serializer/mode — never by sniffing the decrypted bytes or falling back between containers — and **MUST** hard-fail on a parse mismatch. | **Gap:** No encrypted vector decrypts to bytes its reader's container cannot parse, so a reader that falls back to another container on a parse failure, or ignores bytes after the first document, passes every vector. | gap | gap | gap | | [ENC-4](../spec/encryption.md#enc-4) | *`format` tokens*: New serializers **MUST** register their token here before shipping. Tokens are case-sensitive, and the registry is a writer-side contract: … | **Gap:** A rule on how an SDK ships a serializer, which no vector or test can observe; what it protects on the wire is ENC-5. | gap | gap | gap | -| [ENC-5](../spec/encryption.md#enc-5) | *`format` tokens*: … — the registry exists so independent implementations of the same entry agree on the bytes. Writers **MUST** emit only registry tokens; readers SHOULD reject tokens outside the registry before attempting decryption. | `interop-mode.json`: `interop_key_aad`, `interop_encryption_roundtrip`
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `arrow_dataframe_write`
**Gap:** Only cachekit-py's default and Arrow writes, on plaintext frames, and the interop AAD are pinned against the token a writer emits: no vector holds an OrjsonSerializer write or an auto-mode cachekit-ts or cachekit-rs write, and encryption.json takes the token as an input, so a writer that emits an off-registry token in any of those passes. | partial (gap) | partial (2/5) | partial (2/5) | +| [ENC-5](../spec/encryption.md#enc-5) | *`format` tokens*: … — the registry exists so independent implementations of the same entry agree on the bytes. Writers **MUST** emit only registry tokens; readers SHOULD reject tokens outside the registry before attempting decryption. | `interop-mode.json`: `interop_key_aad`, `interop_encryption_roundtrip`
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `arrow_dataframe_write`
**Gap:** Only cachekit-py's default and Arrow writes, on plaintext frames, and the interop AAD are pinned against the token a writer emits: no vector holds an OrjsonSerializer write or an auto-mode cachekit-ts or cachekit-rs write, and encryption.json takes the token as an input, so a writer that emits an off-registry token in any of those passes. | partial (gap) | partial (2/5) | partial (gap) | | [ENC-6](../spec/encryption.md#enc-6) | *`compressed` tokens*: … Python's `str(bool)` output; that coincidence is now frozen. Implementations in every language **MUST** emit exactly these byte sequences — `true`, `false`, `TRUE`, `1`, `0`, or a raw byte are all non-conformant and fail … | `encryption.json`: `basic_bytes`, `special_cache_key`, `compressed_basic`, `arrow_uncompressed`, `arrow_compressed`, `orjson_uncompressed`, `original_type_numpy`, `encrypted_with_k1`, `encrypted_with_k2`, `default_tenant_interop`, `master_key_every_hex_digit`
`interop-mode.json`: `interop_key_aad`, `interop_encryption_roundtrip`
`tools/encryption-verify.py:verify_sealed_vector`
`tools/interop-crosscheck.mjs:aadV3`
**Gap:** The vectors pin the bytes a renderer emits for a JSON boolean, not the AAD an SDK's write path emits: an SDK whose writer spells the token itself, in stored metadata or on a second code path, and whose reader rebuilds the AAD from that spelling round-trips with itself and passes them through whichever renderer its test drives; no SDK can seal with a fixed nonce, so only a test that captures the AAD its write path emits catches it. | partial (2/13) | partial (gap) | partial (2/13) | | [ENC-7](../spec/encryption.md#enc-7) | *Key Rotation (Keyring)*: … master keys — typically previous keys; during a two-phase rollout, the incoming key. SDKs **MUST** reject configuration exceeding the cap at load — never silently truncate. | **Gap:** No vector carries a keyring configuration to reject: the keyring vectors use one current and one decrypt-only key, so an SDK that truncates a fourth decrypt-only key, or rejects it only on first use, passes them. | gap | gap | gap | | [ENC-8](../spec/encryption.md#enc-8) | *Key Rotation (Keyring)*: A master key that has ever occupied the current (encrypting) slot **MUST NOT** be re-promoted to current; it may re-appear only in the decrypt-only list. Backing out a rotation means rotating … | Binds application code.
**Gap:** The spec makes this operator-enforced: nothing an SDK holds shows that a key was once current, and ENC-9 is the part an SDK can detect. | n/a | n/a | n/a | | [ENC-9](../spec/encryption.md#enc-9) | *Key Rotation (Keyring)*: … before, so this invariant is operator-enforced (treat retired key material as destroyed); SDKs **MUST** reject the detectable subset — a configuration where the current key also appears in the decrypt-only list. | **Gap:** No vector carries a keyring configuration to reject, so an SDK that accepts its current key in the decrypt-only list passes every keyring vector. | gap | gap | gap | -| [ENC-10](../spec/encryption.md#enc-10) | *Encryption Flow*: The AAD inputs in step 4 **MUST** reflect what steps 1–2 actually produced — e.g. cachekit-py's default `StandardSerializer` path is `("msgpack", true, … | `interop-mode.json`: `interop_key_aad`, `interop_encryption_roundtrip`
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `arrow_dataframe_write`
**Gap:** Only cachekit-py's StandardSerializer writes, on plaintext frames, and the interop write are pinned against their containers: arrow_dataframe_write checks frame structure, not its IPC bytes, so an Arrow writer that records compressed true over uncompressed buffers passes it; no vector holds a cachekit-ts write with compression on or off, a cachekit-py write with integrity checking off, or an ArrowSerializer write with compression disabled (the case the spec names), so a writer whose flag ignores what its envelope step did passes there. | partial (gap) | partial (2/5) | partial (2/5) | +| [ENC-10](../spec/encryption.md#enc-10) | *Encryption Flow*: The AAD inputs in step 4 **MUST** reflect what steps 1–2 actually produced — e.g. cachekit-py's default `StandardSerializer` path is `("msgpack", true, … | `interop-mode.json`: `interop_key_aad`, `interop_encryption_roundtrip`
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `arrow_dataframe_write`
**Gap:** Only cachekit-py's StandardSerializer writes, on plaintext frames, and the interop write are pinned against their containers: arrow_dataframe_write checks frame structure, not its IPC bytes, so an Arrow writer that records compressed true over uncompressed buffers passes it; no vector holds a cachekit-ts write with compression on or off, a cachekit-py write with integrity checking off, or an ArrowSerializer write with compression disabled (the case the spec names), so a writer whose flag ignores what its envelope step did passes there. | partial (gap) | partial (2/5) | partial (gap) | ## spec/file-backend-format.md | Id | Requirement | Vectors, tests and gaps | cachekit-py | cachekit-ts | cachekit-rs | | :--- | :--- | :--- | :--- | :--- | :--- | -| [FILE-1](../spec/file-backend-format.md#file-1) | *Entry layout*: Writers for version 1 **MUST** set the reserved byte and flags to zero. The expiry is the integer UTC Unix-second deadline. An entry with a nonzero … | `file-backend.json`: `permanent_ascii`, `future_expiry`, `expired_entry`, `empty_payload`, `binary_payload`
**Gap:** Every payload is 256 B or less, so a writer that sets the reserved byte or a flag only above some size passes them; a size threshold has no edge to pin, and a vector large enough to pass a plausible one would put its payload twice in hex (payload_hex, file_hex). binary_payload holds every byte value, so a writer that flags binary or ciphertext-like payloads fails it. | partial (gap) | partial (3/5) | partial (3/5) | -| [FILE-2](../spec/file-backend-format.md#file-2) | *Version and flag negotiation*: … indicate a future payload transform. A reader that does not implement every indicated transform **MUST** return a miss and MUST NOT delete, rewrite, or return the payload. This fails closed rather than exposing transformed … | `file-backend.json`: `unknown_flag_preserved`, `reserved_nonzero_preserved`, `unknown_flag_high_bit`, `reserved_one_preserved`, `reserved_ff_preserved`, `unknown_flag_expired`, `reserved_nonzero_expired` | covered | partial (2/7) | partial (2/7) | -| [FILE-3](../spec/file-backend-format.md#file-3) | *Version and flag negotiation*: … transform. A reader that does not implement every indicated transform MUST return a miss and **MUST NOT** delete, rewrite, or return the payload. This fails closed rather than exposing transformed bytes as plaintext. | `file-backend.json`: `unknown_flag_preserved`, `reserved_nonzero_preserved`, `unknown_flag_high_bit`, `reserved_one_preserved`, `reserved_ff_preserved`, `unknown_flag_expired`, `reserved_nonzero_expired`
**Gap:** The expired vectors differ from a miss_expired read only in the file surviving, so only a test that checks the file afterwards catches a reader that checks expiry first and deletes. No vector refreshes the TTL of an entry with an unknown flag or reserved value. A refresh rewrites bytes 6 to 13, but the rule's object is the payload, which a refresh leaves alone, so whether FILE-3 forbids it is open; every SDK's refresh skips such an entry today. | partial (gap) | partial (2/7) | partial (2/7) | -| [FILE-4](../spec/file-backend-format.md#file-4) | *Version and flag negotiation*: A future nonzero flag assignment requires a protocol update and canonical test vector. A writer **MUST NOT** set an unknown flag. A reader that implements a future flag must preserve the established fields and verify the … | `file-backend.json`: `permanent_ascii`, `future_expiry`, `expired_entry`, `empty_payload`, `binary_payload`
**Gap:** In version 1 every flag is unknown, so this is FILE-1's check and shares its gap. | partial (gap) | partial (3/5) | partial (3/5) | +| [FILE-1](../spec/file-backend-format.md#file-1) | *Entry layout*: Writers for version 1 **MUST** set the reserved byte and flags to zero. The expiry is the integer UTC Unix-second deadline. An entry with a nonzero … | `file-backend.json`: `permanent_ascii`, `future_expiry`, `expired_entry`, `empty_payload`, `binary_payload`
**Gap:** Every payload is 256 B or less, so a writer that sets the reserved byte or a flag only above some size passes them; a size threshold has no edge to pin, and a vector large enough to pass a plausible one would put its payload twice in hex (payload_hex, file_hex). binary_payload holds every byte value, so a writer that flags binary or ciphertext-like payloads fails it. | partial (gap) | partial (3/5) | partial (gap) | +| [FILE-2](../spec/file-backend-format.md#file-2) | *Version and flag negotiation*: … indicate a future payload transform. A reader that does not implement every indicated transform **MUST** return a miss and MUST NOT delete, rewrite, or return the payload. This fails closed rather than exposing transformed … | `file-backend.json`: `unknown_flag_preserved`, `reserved_nonzero_preserved`, `unknown_flag_high_bit`, `reserved_one_preserved`, `reserved_ff_preserved`, `unknown_flag_expired`, `reserved_nonzero_expired` | covered | partial (2/7) | covered | +| [FILE-3](../spec/file-backend-format.md#file-3) | *Version and flag negotiation*: … transform. A reader that does not implement every indicated transform MUST return a miss and **MUST NOT** delete, rewrite, or return the payload. This fails closed rather than exposing transformed bytes as plaintext. | `file-backend.json`: `unknown_flag_preserved`, `reserved_nonzero_preserved`, `unknown_flag_high_bit`, `reserved_one_preserved`, `reserved_ff_preserved`, `unknown_flag_expired`, `reserved_nonzero_expired`
**Gap:** The expired vectors differ from a miss_expired read only in the file surviving, so only a test that checks the file afterwards catches a reader that checks expiry first and deletes. No vector refreshes the TTL of an entry with an unknown flag or reserved value. A refresh rewrites bytes 6 to 13, but the rule's object is the payload, which a refresh leaves alone, so whether FILE-3 forbids it is open; every SDK's refresh skips such an entry today. | partial (gap) | partial (2/7) | partial (gap) | +| [FILE-4](../spec/file-backend-format.md#file-4) | *Version and flag negotiation*: A future nonzero flag assignment requires a protocol update and canonical test vector. A writer **MUST NOT** set an unknown flag. A reader that implements a future flag must preserve the established fields and verify the … | `file-backend.json`: `permanent_ascii`, `future_expiry`, `expired_entry`, `empty_payload`, `binary_payload`
**Gap:** In version 1 every flag is unknown, so this is FILE-1's check and shares its gap. | partial (gap) | partial (3/5) | partial (gap) | | [FILE-5](../spec/file-backend-format.md#file-5) | *Write and TTL behavior*: … directory and atomically renamed into place. Refreshing TTL rewrites only bytes 6 through 13 and **MUST NOT** expose a torn expiry to a concurrent reader: a refresh either rewrites the entry through the same temporary-file and … | **Gap:** A torn expiry shows only while a refresh races a read; no byte vector can show it. | gap | gap | gap | | [FILE-6](../spec/file-backend-format.md#file-6) | *Write and TTL behavior*: … guarantee read/write atomicity for regular files (POSIX.1-2017 §2.9.7) — in which case readers **MUST** load the 14-byte header in a single read. Writers calculate the absolute deadline and store its whole-second Unix … | **Gap:** How many reads load the header is the reader's I/O, which no byte vector can show. | gap | gap | gap | @@ -175,8 +175,8 @@ Marked not-a-requirement in `spec/intent-presets.md`: | [IOP-16](../spec/interop-mode.md#iop-16) | *Number canonicalization*: … range (snowflake IDs work). In JavaScript, integers above 2⁵³ MUST be handled as `BigInt`; the SDK **MUST** error on a non-integral-safe `Number` rather than silently rounding. | Binds cachekit-ts only.
`tools/interop-crosscheck.mjs:unsafeNumberSelftest`
**Gap:** The self-test exercises the reference cross-check, not cachekit-ts: Python and Rust read a bare JSON integer beyond 2^53 exactly, so no shared vector can carry the unsafe Number only JavaScript would see. | n/a | uncovered | n/a | | [IOP-17](../spec/interop-mode.md#iop-17) | *Interop Value Format*: Writers SHOULD emit canonical encoding (shortest forms, sorted map keys) and **MUST** do so to match the published value vectors. Unlike the args profile, the value profile does not apply number … | `interop-mode.json`: `issue_example_object`, `float_value_stays_float64`, `mixed_array`, `datetime_sentinel_value`, `proto_key_value`, `bom_strings_value`
**Gap:** The vectors pin the bytes an SDK's value encoder emits for each value, not the bytes its write path stores: an SDK whose write path serializes through a general MessagePack library instead, taking map order and number widths from the caller's type, passes them through whichever encoder its test drives; only a test that writes each value through the SDK's write path and compares the stored bytes catches it. | partial (gap) | partial (gap) | partial (gap) | | [IOP-18](../spec/interop-mode.md#iop-18) | *Interop Value Format*: Readers **MUST** accept any well-formed MessagePack document, canonical or not — and MUST consume exactly one document, rejecting … | `interop-mode.json`: `issue_example_object`, `float_value_stays_float64`, `mixed_array`, `datetime_sentinel_value`, `proto_key_value`, `bom_strings_value`, `reader_padded_int_widths`, `reader_padded_headers`, `reader_unsorted_map_keys`, `reader_float32`, `reader_non_string_map_key`, `reader_ext_type`
`decode-bounds.json`: `nested_fixarray_depth_32`, `nested_fixmap_depth_32`, `array16_256_backed_nils`
**Gap:** The reader vectors hold one non-string map key, an integer that fits a fixint, and stock decoders reject other well-formed keys: @msgpack/msgpack 3.1.3 throws on a nil, bool, bin, array or map key and on an integer key written at 64-bit width, and msgpack-python with strict_map_key=False on an array or map key, so a reader built on either passes every vector without accepting every well-formed document. | partial (gap) | partial (gap) | partial (gap) | -| [IOP-19](../spec/interop-mode.md#iop-19) | *Interop Value Format*: Readers MUST accept any well-formed MessagePack document, canonical or not — and **MUST** consume exactly one document, rejecting trailing bytes. This strictness is load-bearing: a Python auto-mode CK frame … | `interop-mode.json`: `reader_trailing_byte`
`python-frame.json`: `ck_frame_fed_to_interop_reader` | covered | partial (1/2) | partial (1/2) | -| [IOP-20](../spec/interop-mode.md#iop-20) | *Interop Value Format*: … does NOT — it deserializes one value and ignores trailing bytes — so a Rust interop reader **MUST** add an explicit end-of-input check (e.g. drive a `Deserializer::from_read_ref` and verify the input is exhausted). On … | Binds cachekit-rs only.
`interop-mode.json`: `reader_trailing_byte`
`python-frame.json`: `ck_frame_fed_to_interop_reader` | n/a | n/a | partial (1/2) | +| [IOP-19](../spec/interop-mode.md#iop-19) | *Interop Value Format*: Readers MUST accept any well-formed MessagePack document, canonical or not — and **MUST** consume exactly one document, rejecting trailing bytes. This strictness is load-bearing: a Python auto-mode CK frame … | `interop-mode.json`: `reader_trailing_byte`
`python-frame.json`: `ck_frame_fed_to_interop_reader` | covered | partial (1/2) | covered | +| [IOP-20](../spec/interop-mode.md#iop-20) | *Interop Value Format*: … does NOT — it deserializes one value and ignores trailing bytes — so a Rust interop reader **MUST** add an explicit end-of-input check (e.g. drive a `Deserializer::from_read_ref` and verify the input is exhausted). On … | Binds cachekit-rs only.
`interop-mode.json`: `reader_trailing_byte`
`python-frame.json`: `ck_frame_fed_to_interop_reader` | n/a | n/a | covered | | [IOP-21](../spec/interop-mode.md#iop-21) | *SDK Implementation Requirements*: An SDK implementation of interop mode **MUST**: | `interop-mode.json` (all 93)
`interop-mode.json`: `small_int_as_bigint`
**Gap:** The list's demands on segment validation, normalization, value encoding and hashing share the holes still recorded on IOP-2, IOP-8, IOP-10, IOP-13, IOP-16 and IOP-17; no vector reaches the 32-bit length tier (untested by design); binding named and defaulted arguments to their positions is an SDK API property no vector observes; leaving auto mode unchanged is pinned only where an SDK vendors cache-keys.json or wire-format.json. | partial (gap) | partial (gap) | partial (gap) | | [IOP-22](../spec/interop-mode.md#iop-22) | *SDK Implementation Requirements*: Pass every vector in `test-vectors/interop-mode.json`, including the `error_vectors` (which **MUST** raise) and — when the SDK supports encryption — the `interop_encryption_roundtrip` decrypt. | `interop-mode.json`: `reject_nan`, `reject_infinity`, `reject_negative_infinity`, `reject_int_overflow`, `reject_int_underflow`, `reject_naive_datetime`, `reject_uppercase_namespace`, `reject_colon_in_operation`, `reject_trailing_newline`, `reject_reserved_namespace_ns`, `reject_reserved_namespace_nsapi`, `reject_double_dot_namespace`, `reject_double_dot_operation`, `reject_namespace_too_long`, `reject_operation_too_long`, `reject_leading_dot_namespace`, `reject_leading_hyphen_operation`, `reject_leading_underscore_operation`, `reject_empty_namespace`, `reject_empty_operation`, `reject_non_ascii_digit_namespace`, `reject_non_ascii_letter_operation`, `reject_fullwidth_operation`, `reject_slash_namespace`, `reject_space_operation`, `reject_uppercase_operation`, `reject_colon_in_namespace`, `reject_trailing_newline_operation`, `reject_naive_datetime_nested`, `reject_nan_nested`, `reject_int_overflow_in_list`, `reject_int_underflow_in_map`, `reject_int_overflow_in_map`, `reject_int_underflow_in_list` | covered | covered | covered | | [IOP-23](../spec/interop-mode.md#iop-23) | *SDK Implementation Requirements*: … The same applies to UUIDs passed as plain strings (TypeScript has no UUID type): callers **MUST** use the lowercase hyphenated form, or `"550E8400-…"` from TS will silently miss the key a Python `uuid.UUID` argument … | Binds application code.
**Gap:** A UUID passed as a plain string reaches the SDK as an ordinary string, so neither the SDK nor a vector can see how it was spelled. | n/a | n/a | n/a | @@ -302,7 +302,7 @@ Marked not-a-requirement in `spec/saas-api.md`: | [WIRE-18](../spec/wire-format.md#wire-18) | *Reject vectors*: … MUST also bound what the read allocates, and fail if it reaches `original_size`. The measurement **MUST** satisfy all of these: | Binds cachekit-py, cachekit-ts only.
`tools/test_wire_format_reference.py:_check_allocation_probe`
**Gap:** The probe is SDK test code no vector can check; the reference tool's probe and its positive control measure the reference reader only. | uncovered | uncovered | n/a | | [WIRE-19](../spec/wire-format.md#wire-19) | *Reject vectors*: … is a strict slice of an envelope that step 1 has already bounded to 512 MiB. An implementation **MUST** still enforce both. Test step 1 with a lowered cap, as `check_reader_rejects` in `tools/test_wire_format_reference.py` … | Binds cachekit-py, cachekit-ts only.
`tools/test_wire_format_reference.py:check_reader_rejects`
**Gap:** No vector covers step 1, which needs an envelope over 512 MiB, or step 3, which cannot fire once step 1 has passed. The reference tool reaches step 1 by lowering its cap; for cachekit-py and cachekit-ts, whose step 1 runs in cachekit-core behind compile-time caps, only an envelope over 512 MiB reaches it. | uncovered | uncovered | n/a | | [WIRE-20](../spec/wire-format.md#wire-20) | *Datetime Extension Format*: All SDKs **MUST** check for these sentinel keys during deserialization and reconstruct the appropriate temporal type. Failing to handle … | `wire-format.json`: `temporal_sentinel_datetime`, `temporal_sentinel_date`, `temporal_sentinel_time`
**Gap:** The vectors pin the three canonical maps, with a whole-second UTC datetime, so a reviver that drops a non-UTC offset or sub-second digits passes; whether a JavaScript Date's millisecond precision meets the rule is not specified. A map's shape beyond them (an extra key, a marker other than true) is not specified either, so nothing pins it. cachekit-ts and cachekit-rs do not revive them in auto mode (sdk-feature-matrix.md). | partial (gap) | uncovered (0/3) | uncovered (0/3) | -| [WIRE-21](../spec/wire-format.md#wire-21) | *SDK Storage Containers (auto mode)*: … wire formats and never will be — cross-SDK sharing goes through interop mode exclusively. An SDK **MUST NOT** decode another SDK's auto-mode container. An SDK MAY parse a foreign container *for diagnostics only*, using the … | `python-frame.json`: `ck_frame_fed_to_interop_reader`, `bare_envelope_fed_to_frame_reader`, `plain_msgpack_fed_to_frame_reader`
**Gap:** cachekit-py's refusal of a bare envelope and of plain MessagePack is pinned, and so is a plain-MessagePack reader's refusal of a CK frame, by its trailing bytes. No vector names cachekit-ts's envelope reader as the one refusing a CK frame or plain MessagePack, though the vectors carry both, and none can tell cachekit-ts's uncompressed container from cachekit-rs's: both are plain MessagePack. | partial (gap) | uncovered (0/3) | uncovered (0/3) | +| [WIRE-21](../spec/wire-format.md#wire-21) | *SDK Storage Containers (auto mode)*: … wire formats and never will be — cross-SDK sharing goes through interop mode exclusively. An SDK **MUST NOT** decode another SDK's auto-mode container. An SDK MAY parse a foreign container *for diagnostics only*, using the … | `python-frame.json`: `ck_frame_fed_to_interop_reader`, `bare_envelope_fed_to_frame_reader`, `plain_msgpack_fed_to_frame_reader`
**Gap:** cachekit-py's refusal of a bare envelope and of plain MessagePack is pinned, and so is a plain-MessagePack reader's refusal of a CK frame, by its trailing bytes. No vector names cachekit-ts's envelope reader as the one refusing a CK frame or plain MessagePack, though the vectors carry both, and none can tell cachekit-ts's uncompressed container from cachekit-rs's: both are plain MessagePack. cachekit-rs decodes bare_envelope_fed_to_frame_reader, a WIRE-21 violation (sdk-feature-matrix.md note 15). | partial (gap) | uncovered (0/3) | partial (gap) | | [WIRE-22](../spec/wire-format.md#wire-22) | *Python: CK v3 frame*: A reader **MUST** reject: frames shorter than the 7-byte fixed prefix, `VERSION != 3`, and a declared `HDR_LEN` that overruns the value. … | Binds cachekit-py only.
`python-frame.json`: `truncated_frame`, `unsupported_frame_version`, `header_overrun`, `truncated_frame_one_short`, `unsupported_frame_version_2`, `unsupported_frame_version_4`, `header_overrun_by_one`
`tools/python-frame-reference.py:verify`
**Gap:** unsupported_frame_version_2 and unsupported_frame_version_4 fail a reader that rejects only versions above or below 3, since their header and payload are intact. The other boundary vectors do not isolate their checks: a reader that accepts a 6-byte frame rejects truncated_frame_one_short at the header-length check, and one that slices an overrunning header to the bytes present gets header_overrun_by_one's real header and an empty payload, which its envelope decode rejects. Only a test that asserts the frame parser's own error, the check rejected_by names, catches either. | partial (gap) | n/a | n/a | | [WIRE-23](../spec/wire-format.md#wire-23) | *Python: CK v3 frame*: A reader configured for encryption **MUST NOT** let the header's `encrypted` / `s` / `m` values downgrade it to a non-authenticated read path. If the cache is … | Binds cachekit-py only.
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `forged_plaintext_encrypted_false`, `forged_plaintext_orjson`
**Gap:** The forged frames claim plaintext two ways, an explicit "encrypted": false and another serializer's plaintext write, so a reader that trusts either claim returns the value; forged_plaintext_orjson also trips a default-configured reader's serializer-name check first, so on its own it does not show the downgrade check. No vector alters an m value the AAD binds (format, compressed, original_type) on a real ciphertext, which authentication then rejects. | partial (gap) | n/a | n/a | | [WIRE-24](../spec/wire-format.md#wire-24) | *Python: CK v3 frame*: … If the cache is configured encrypted and an entry does not authenticate as ciphertext, the read **MUST** fail closed — an attacker with backend write access (the CVSS 8.5 actor in encryption.md's threat model) must not be … | Binds cachekit-py only.
`python-frame.json`: `default_saas_write_msgpack_bytestorage`, `default_saas_write_msgpack_bytestorage_bin`, `forged_plaintext_encrypted_false`, `forged_plaintext_orjson`, `ciphertext_key_not_in_keyring`, `ciphertext_other_tenant`
**Gap:** ciphertext_key_not_in_keyring fails every reader that does not return plaintext, so it pins the keyring path's outcome, not a check a wrong reader skips. ciphertext_other_tenant fails closed for a reader that resolves its own tenant (encrypted_reader's tenant_source); one that takes the tenant from the frame header decrypts it. Whether a single-tenant reader may take the header's tenant is open: spec/encryption.md's Decryption Flow takes tenant_id as an input without saying where it comes from. | partial (gap) | n/a | n/a | diff --git a/conformance/requirements.json b/conformance/requirements.json index f189259..0922629 100644 --- a/conformance/requirements.json +++ b/conformance/requirements.json @@ -2381,7 +2381,7 @@ "python-frame.json:bare_envelope_fed_to_frame_reader", "python-frame.json:plain_msgpack_fed_to_frame_reader" ], - "gap": "cachekit-py's refusal of a bare envelope and of plain MessagePack is pinned, and so is a plain-MessagePack reader's refusal of a CK frame, by its trailing bytes. No vector names cachekit-ts's envelope reader as the one refusing a CK frame or plain MessagePack, though the vectors carry both, and none can tell cachekit-ts's uncompressed container from cachekit-rs's: both are plain MessagePack." + "gap": "cachekit-py's refusal of a bare envelope and of plain MessagePack is pinned, and so is a plain-MessagePack reader's refusal of a CK frame, by its trailing bytes. No vector names cachekit-ts's envelope reader as the one refusing a CK frame or plain MessagePack, though the vectors carry both, and none can tell cachekit-ts's uncompressed container from cachekit-rs's: both are plain MessagePack. cachekit-rs decodes bare_envelope_fed_to_frame_reader, a WIRE-21 violation (sdk-feature-matrix.md note 15)." }, "WIRE-22": { "section": "Python: CK v3 frame", diff --git a/conformance/sdks.json b/conformance/sdks.json index c52b7a9..f605b9e 100644 --- a/conformance/sdks.json +++ b/conformance/sdks.json @@ -28,13 +28,14 @@ }, "cachekit-rs": { "repository": "https://github.com/cachekit-io/cachekit-rs", - "commit": "a7feb62b5b589ba6126c20fa36b3f55bf02e82a2", + "commit": "b5fb2a67392ce8dfd4dc199ad7ec7233a661d43b", "fixtures": { "decode-bounds.json": "c52c27f724fe138e63440dc0306936b48fe389e2bd823c058b86b30d854e2e2e", - "file-backend.json": "8d9d8c4709baf9ef3a8f2d71d21fb5a56207bc7a05c9bc7a967ac567f2604615", + "file-backend.json": "b7b0c51935a3a00ab340005ae1e091797b14938f92eecae23346bdccf0d12af4", "freshness-headers.json": "74beb975b6855f52d9dd453279873faf8048a9c3c79a8167cfda099beec5fcd3", "interop-mode.json": "e1ca6c2361509f347d17f3352e0d7ab4d4b61488737bdf0056bb5769d9794e72", - "path-encoding.json": "807af0d39ccf3f5afda65577787ac5adcd01ff6617468060161c84267fa7e0cb" + "path-encoding.json": "807af0d39ccf3f5afda65577787ac5adcd01ff6617468060161c84267fa7e0cb", + "python-frame.json": "1210a2cdf00ef420e59d4d1c75f4979385ad1cb023181b39f46d7f994761770a" } } } diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 5b3ef53..bf48025 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -286,7 +286,7 @@ its spec: 3. **Encryption** — AES-256-GCM with HKDF-SHA256 and AAD v0x03 ([spec/encryption.md](spec/encryption.md)) 4. **SaaS API** — REST endpoints per [spec/saas-api.md](spec/saas-api.md) -**Cross-SDK interoperability** is a separate, narrower contract: [interop mode](spec/interop-mode.md) — language-neutral keys plus plain-MessagePack values with **no** envelope or container (protocol#11). Auto-mode storage containers are SDK-internal and are NOT required of, or readable by, other SDKs. +**Cross-SDK interoperability** is a separate, narrower contract: [interop mode](spec/interop-mode.md) — language-neutral keys plus plain-MessagePack values with **no** envelope or container (protocol#11). Auto-mode storage containers are SDK-internal and are NOT required of, or readable by, other SDKs (cachekit-rs reads cachekit-ts's bare envelope; see ¹⁵). ### Compliance Status @@ -294,21 +294,21 @@ its spec: | :--- | :---: | :---: | :---: | :---: | | Key generation (Blake2b) | ✅ Compliant | N/A auto mode¹⁴ — interop/v1 keygen ✅ merged ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)); `#[cachekit]` mints interop keys ([#35](https://github.com/cachekit-io/cachekit-rs/pull/35)) | ✅ Compliant | ⚠️ Untested | | Wire format (ByteStorage) | ⚠️ Compliant¹⁵ except the envelope pre-scan: payload decodes are pre-scanned (`unpackb_bounded`), but the envelope goes through `cachekit-core`'s `ByteStorage::retrieve`, which has no [Retrieve Flow](spec/wire-format.md#retrieve-flow) step-2 pre-scan in any published core; it is on core `main` ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), unreleased, and this cell flips when cachekit-py bumps to a core release that carries it; ✅ runs all six `wire-format.json` 1.3.0 [Reject vectors](spec/wire-format.md#reject-vectors) through `ByteStorage.retrieve` since 0.21.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); `TestRejectVectors` in `tests/unit/protocol/test_envelope_wire_vectors.py`), asserting the error each row names, except that core shares one error between its zero-length and ratio checks, so the zero-length vector asserts the ratio message; all fourteen `wire-format.json` 1.4.0 reject vectors in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550), unreleased, each asserting the error its row names (the five step-2 typed-decode vectors the typed-decode error, `reject_original_size_sign_bit` the range-checked `u32` decode), with `reject_envelope_slots_overclaim` a strict expected failure (WIRE-9) until cachekit-py pins a core release with the pre-scan, and both `payload_reject_vectors` asserting the structural guard's own error after the envelope read; ⚠️ the allocation bound for the size-cap and ratio vectors needs a probe on `cachekit-core`'s allocator, which no core release has | 🚧 unreleased — Canonical (`cachekit-core`), unused for stored values¹⁵; step-2 pre-scan on core `main` ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)): `ByteStorage::retrieve` and `validate` pre-scan the envelope bytes before the typed `rmp_serde::from_slice` into `StorageEnvelope`; absent from crates.io 0.6.0 | ⚠️ Compliant except the envelope pre-scan: payload decodes are pre-scanned, but the envelope goes through `cachekit-core`'s `ByteStorage::retrieve` (`unpack` in both `cachekit-core-ts` and `cachekit-core-wasm`), which has no step-2 pre-scan in any published core; it is on core `main` ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), unreleased, and this cell flips when both bindings move to a core release that carries it; 🚧 unreleased — on `main`, runs all six `wire-format.json` 1.3.0 [Reject vectors](spec/wire-format.md#reject-vectors) on the envelope read path, asserting the error each row names, in the Node lane (`packages/cachekit/test/protocol/wire-format.protocol.test.ts`) and the Workers lane (`packages/cachekit/test/workers/wire-format.workers.test.ts`) ([cachekit-ts#198](https://github.com/cachekit-io/cachekit-ts/pull/198)); ⚠️ the allocation bound for the size-cap and ratio vectors needs a probe on `cachekit-core`'s allocator, which no core release has | ⚠️ Untested | -| Storage container (auto mode)¹⁵ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | -| Temporal sentinels ([spec](spec/wire-format.md#datetime-extension-format)) | ✅ PyPI 0.22.0 (wheel run 2026-10-07): the default path revives `__datetime__`, `__date__` and `__time__` (each `temporal_sentinel_vectors` payload under `StandardSerializer`), as do `AutoSerializer` and interop mode, and all three write them; the three `wire-format.json` 1.4.0 `temporal_sentinel_vectors` run in CI since [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550), unreleased | ❌ crates.io 0.10.0 (`.crate` inspected 2026-10-07): revives none and writes none, in either mode; interop's value profile refuses a datetime | ❌ npm 0.1.5 (`.tgz` inspected 2026-10-07): the default path revives none, and writes a `Date` as its ISO string; interop mode revives only `__datetime__`, leaving `__date__` and `__time__` as maps, since JavaScript has no date-only or time-only type | ⚠️ Untested | +| Storage container (auto mode)¹⁵ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope; ⚠️ decodes cachekit-ts's bare ByteStorage envelope ([WIRE-21](spec/wire-format.md#wire-21)), in every release and on `main`¹⁵ | Bare ByteStorage envelope (default) | — | +| Temporal sentinels ([spec](spec/wire-format.md#datetime-extension-format)) | ✅ PyPI 0.22.0 (wheel run 2026-10-07): the default path revives `__datetime__`, `__date__` and `__time__` (each `temporal_sentinel_vectors` payload under `StandardSerializer`), as do `AutoSerializer` and interop mode, and all three write them; the three `wire-format.json` 1.4.0 `temporal_sentinel_vectors` run in CI since [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550), unreleased | ❌ crates.io 0.10.0 (`.crate` inspected 2026-10-07): revives none and writes none, in either mode; interop's value profile refuses a datetime; `main` at `b5fb2a6` likewise (no sentinel reviver in `src/`). cachekit-rs vendors no `wire-format.json` | ❌ npm 0.1.5 (`.tgz` inspected 2026-10-07): the default path revives none, and writes a `Date` as its ISO string; interop mode revives only `__datetime__`, leaving `__date__` and `__time__` as maps, since JavaScript has no date-only or time-only type | ⚠️ Untested | | Encryption (AES-256-GCM) | ✅ Compliant | ✅ Canonical (cachekit-core) | ✅ Compliant | ⚠️ Untested | | AAD v0x03 | ⚠️ Compliant (5 components — every auto serializer appends `original_type`; interop mode is the sole 4-component path) except the `cache_key` component behind a prefixing backend: through `cachekit` 0.22.0 it omits the backend's key prefix (`MemcachedBackend` `key_prefix`, the tenant-scoped Redis backend's `t:{tenant}:`), short of the [logical backend key](spec/encryption.md#additional-authenticated-data-aad); bound in [cachekit-py#518](https://github.com/cachekit-io/cachekit-py/pull/518), unreleased (see the Encryption table) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | | SaaS API | ⚠️ Compliant (CachekitIO backend) except, through `cachekit` 0.22.0 (wheel inspected 2026-10-05): a PUT with a TTL also sends the legacy `X-TTL` ([API-44](spec/saas-api.md#api-44)); any fractional `ttl`, the window `@cache.io` derives from it, and a fractional `refresh_ttl` value go on the wire unrounded, so the server answers `400` and the write is lost ([API-42](spec/saas-api.md#api-42), [API-54](spec/saas-api.md#api-54)); both fixed in [cachekit-py#520](https://github.com/cachekit-io/cachekit-py/pull/520), unreleased. Where the [TTL rules](spec/saas-api.md#put-v1cachekey) have the SDK reject a TTL with an error to the caller, `ttl=0` and a TTL over 2,592,000 are sent instead, so the server answers `400` and the write is lost | ⚠️ Compliant (CachekitIO backend) except, through `cachekit-rs` 0.10.0 (`.crate` inspected 2026-10-05): `refresh_ttl` on the CachekitIO and Workers backends rejects a sub-second TTL instead of sending 1 ([API-42](spec/saas-api.md#api-42); `PATCH /ttl` takes the `X-CacheKit-TTL` rules), fixed in [cachekit-rs#142](https://github.com/cachekit-io/cachekit-rs/pull/142), unreleased; and a TTL over 2,592,000 is sent, where the [TTL rules](spec/saas-api.md#put-v1cachekey) have the SDK reject it, so the server answers `400` and the write is lost (`validate_ttl` rejects only zero) | ⚠️ through npm 0.1.5 (`.tgz` inspected 2026-10-05) a PUT sends only the legacy `X-TTL` ([API-44](spec/saas-api.md#api-44)), unrounded, omits it for `ttl` 0, storing the entry with no expiry, and sends a TTL over 2,592,000 for the server to reject with `400`; on `main` since [cachekit-ts#110](https://github.com/cachekit-io/cachekit-ts/pull/110), unreleased, a PUT sends `X-CacheKit-TTL` ceiled to whole seconds and rejects 0 and an over-cap TTL. Every release and `main`: a `200` lock response whose `lock_id` is absent, empty or not a string counts as a lease, so single-flight is off and the release sends `undefined` as the lock id ([API-65](spec/saas-api.md#api-65)); fixed in [cachekit-ts#215](https://github.com/cachekit-io/cachekit-ts/pull/215), unreleased | ❌ Not implemented | | SaaS API — cache-key path encoding ([spec](spec/saas-api.md#cache-key-path-encoding)) | ✅ Compliant on `main`, unreleased — rejects the empty key ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `.`, `..`, `health`, `ttl` and `lock` since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); rules 1/3/4 since 0.18.0 ([cachekit-py#279](https://github.com/cachekit-io/cachekit-py/pull/279)) | ✅ Compliant on `main`, unreleased ([cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100), which adds the empty key); rejects `.`, `..`, `health`, `ttl` and `lock` since `cachekit-rs` 0.8.0 ([cachekit-rs#76](https://github.com/cachekit-io/cachekit-rs/pull/76); `.crate` inspected 2026-09-30) | ✅ Compliant on `main`, unreleased ([cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118)) | ❌ Not implemented | -| Test vectors in CI¹⁶ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) — fixture 1.1.0 (`ns`/`nsapi` namespace reservation) since PyPI 0.20.0 ([cachekit-py#350](https://github.com/cachekit-io/cachekit-py/pull/350)); fixture 1.2.0 (no `..` in a segment) since 0.21.0 ([cachekit-py#391](https://github.com/cachekit-io/cachekit-py/pull/391)); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), unreleased, its key, value and error vectors and both reader groups through the interop key derivation and value reader; `decode-bounds.json` vendored + CI-executed since [cachekit-py#276](https://github.com/cachekit-io/cachekit-py/pull/276) (LAB-2503); `1.1.0` since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)); `1.2.0` (the `incomplete` reason) vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), unreleased, both `incomplete` vectors included; every version asserting the structural guard's own error for every reject vector at every payload read path (the [Decode bounds](spec/interop-mode.md#decode-bounds) MUST) — ⚠️ except the envelope entry point (`ByteStorage::retrieve`), which has no guard to assert until cachekit-py moves to a core release carrying [cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80) (see the Wire format row); `path-encoding.json` vendored + CI-executed since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); fixture 1.1.0 (empty-key row) since 0.21.0 ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `path-encoding.json` 1.2.0 and `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522), unreleased; `cache-keys.json` 1.1.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)), `file-backend.json` 1.1.0 ([cachekit-py#431](https://github.com/cachekit-io/cachekit-py/pull/431)) and `wire-format.json` 1.3.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); its reject vectors: see the Wire format row) vendored byte for byte (sha256-pinned) + CI-executed, each since 0.21.0; `wire-format.json` 1.4.0 (every new accept, constructed, reject, payload-reject and temporal vector), `file-backend.json` 1.2.0 (every `miss_preserve` vector on all five read paths, the expired flagged entries included, and every `return_payload` vector) and `python-frame.json` (first vendor: every frame vector through `SerializationWrapper.unwrap`, every error vector at the check its `rejected_by` names, the CK frame through the interop reader, and every `encrypted_read_vectors` frame failing closed under both tamper policies with the tenant resolved by the reader) vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550), unreleased | ✅ interop/v1 (full set) since [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) — fixture 1.1.0 since `cachekit-rs` 0.8.0 ([cachekit-rs#89](https://github.com/cachekit-io/cachekit-rs/pull/89); `.crate` inspected 2026-09-30); fixture 1.2.0 since 0.9.0 ([cachekit-rs#98](https://github.com/cachekit-io/cachekit-rs/pull/98); `.crate` inspected 2026-10-06); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), unreleased, both reader groups through `interop::deserialize` and every value vector read back through it; `decode-bounds.json` vendored + CI-executed since [cachekit-rs#73](https://github.com/cachekit-io/cachekit-rs/pull/73) (LAB-2503; default CI green on `main`); `1.1.0` since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)); `1.2.0` (the `incomplete` reason) in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), unreleased, asserting the structural guard's own `decode bound:` error for every reject vector through both decoders and `get` / `interop_get` / `interop_get_swr` — the guard enforces depth itself since [cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93) (no envelope entry point¹⁵); `path-encoding.json` 1.1.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100), unreleased, driving both the `encode_key` and the URL-builder tests; fixture 1.2.0 (encoder-mistake rows) and `freshness-headers.json` 1.0.0, vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146), unreleased, every freshness row run through the `GET` response's header reader (`freshness_from_headers`); `file-backend.json` 1.1.0 vendored byte for byte + CI-executed since 0.9.0 ([cachekit-rs#107](https://github.com/cachekit-io/cachekit-rs/pull/107); `.crate` inspected 2026-10-05), by tests in `src/backend/file.rs` behind the `file` feature, which default CI enables | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors — fixture 1.1.0 in [cachekit-ts#143](https://github.com/cachekit-io/cachekit-ts/pull/143), unreleased; fixture 1.2.0 in [cachekit-ts#164](https://github.com/cachekit-io/cachekit-ts/pull/164), unreleased; fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), unreleased: the key, value and error vectors and both reader groups, every value vector read back through `decodeInteropValue` as its value, with the reader fixes two of them needed (a `__proto__` map key reads as an ordinary key, and a leading U+FEFF is kept); decode bounds enforced ([#112](https://github.com/cachekit-io/cachekit-ts/pull/112)); `decode-bounds.json` vendored + CI-executed since [cachekit-ts#121](https://github.com/cachekit-io/cachekit-ts/pull/121) (LAB-2737), `1.1.0` in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152), unreleased; `1.2.0` (the `incomplete` reason) in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), unreleased, both incomplete vectors through all three untrusted decode entry points (`MessagePackSerializer.decode`, `decodeInteropValue`, `deserializeEvent`), asserting the pre-scan's own error; every reject vector asserts the guard error it trips (pre-scan, or the event size cap ahead of it) — ⚠️ except the envelope entry point (`unpack` in `cachekit-core-ts` and `cachekit-core-wasm`), which has no guard to assert until both bindings move to a core release carrying [cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80) and the vectors run through `unpack` (see the Wire format row); for the three reject vectors over the invalidation event entry point's (`deserializeEvent`) 4096 B size cap, the test also calls that site's structural guard (`assertDecodeDepth`, at depth 3) directly and asserts its rejection, as [Decode bounds](spec/interop-mode.md#decode-bounds) requires where a size cap rejects first, in [cachekit-ts#182](https://github.com/cachekit-io/cachekit-ts/pull/182), unreleased; `path-encoding.json` vendored + CI-executed since [cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118), unreleased; fixture 1.1.0 (empty-key row) in [cachekit-ts#165](https://github.com/cachekit-io/cachekit-ts/pull/165), unreleased; fixture 1.2.0 (encoder-mistake rows) in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), unreleased, every row through `encodeKey` and all eight request-path operations; `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), unreleased, each row through a real `Headers` and `freshnessFromHeaders`; `wire-format.json` 1.3.0 vendored byte for byte + CI-executed in the Node and Workers lanes in [cachekit-ts#184](https://github.com/cachekit-io/cachekit-ts/pull/184), unreleased (its reject vectors: see the Wire format row); `encryption.json` vendored + CI-executed in the Workers lane since 0.1.4 ([cachekit-ts#78](https://github.com/cachekit-io/cachekit-ts/pull/78)); `1.2.0`, with its `keyring` and `default_tenant` groups, re-vendored byte for byte (sha256-pinned) in [cachekit-ts#211](https://github.com/cachekit-io/cachekit-ts/pull/211), unreleased; `1.3.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222), unreleased, its accept and eleven hex reject master key input rows driven through `createCache.secure` with the key passed both as `masterKey` and through `CACHEKIT_MASTER_KEY` (cachekit-ts has no raw-key entry point, so the raw-bytes rows do not apply) | ⚠️ Pending | +| Test vectors in CI¹⁶ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) — fixture 1.1.0 (`ns`/`nsapi` namespace reservation) since PyPI 0.20.0 ([cachekit-py#350](https://github.com/cachekit-io/cachekit-py/pull/350)); fixture 1.2.0 (no `..` in a segment) since 0.21.0 ([cachekit-py#391](https://github.com/cachekit-io/cachekit-py/pull/391)); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), unreleased, its key, value and error vectors and both reader groups through the interop key derivation and value reader; `decode-bounds.json` vendored + CI-executed since [cachekit-py#276](https://github.com/cachekit-io/cachekit-py/pull/276) (LAB-2503); `1.1.0` since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)); `1.2.0` (the `incomplete` reason) vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), unreleased, both `incomplete` vectors included; every version asserting the structural guard's own error for every reject vector at every payload read path (the [Decode bounds](spec/interop-mode.md#decode-bounds) MUST) — ⚠️ except the envelope entry point (`ByteStorage::retrieve`), which has no guard to assert until cachekit-py moves to a core release carrying [cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80) (see the Wire format row); `path-encoding.json` vendored + CI-executed since 0.20.0 ([cachekit-py#364](https://github.com/cachekit-io/cachekit-py/pull/364)); fixture 1.1.0 (empty-key row) since 0.21.0 ([cachekit-py#397](https://github.com/cachekit-io/cachekit-py/pull/397)); `path-encoding.json` 1.2.0 and `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522), unreleased; `cache-keys.json` 1.1.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)), `file-backend.json` 1.1.0 ([cachekit-py#431](https://github.com/cachekit-io/cachekit-py/pull/431)) and `wire-format.json` 1.3.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448); its reject vectors: see the Wire format row) vendored byte for byte (sha256-pinned) + CI-executed, each since 0.21.0; `wire-format.json` 1.4.0 (every new accept, constructed, reject, payload-reject and temporal vector), `file-backend.json` 1.2.0 (every `miss_preserve` vector on all five read paths, the expired flagged entries included, and every `return_payload` vector) and `python-frame.json` (first vendor: every frame vector through `SerializationWrapper.unwrap`, every error vector at the check its `rejected_by` names, the CK frame through the interop reader, and every `encrypted_read_vectors` frame failing closed under both tamper policies with the tenant resolved by the reader) vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550), unreleased | ✅ interop/v1 (full set) since [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) — fixture 1.1.0 since `cachekit-rs` 0.8.0 ([cachekit-rs#89](https://github.com/cachekit-io/cachekit-rs/pull/89); `.crate` inspected 2026-09-30); fixture 1.2.0 since 0.9.0 ([cachekit-rs#98](https://github.com/cachekit-io/cachekit-rs/pull/98); `.crate` inspected 2026-10-06); fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), unreleased, both reader groups through `interop::deserialize` and every value vector read back through it; `decode-bounds.json` vendored + CI-executed since [cachekit-rs#73](https://github.com/cachekit-io/cachekit-rs/pull/73) (LAB-2503; default CI green on `main`); `1.1.0` since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)); `1.2.0` (the `incomplete` reason) in [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152), unreleased, asserting the structural guard's own `decode bound:` error for every reject vector through both decoders and `get` / `interop_get` / `interop_get_swr` — the guard enforces depth itself since [cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93) (no envelope entry point¹⁵); `path-encoding.json` 1.1.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#100](https://github.com/cachekit-io/cachekit-rs/pull/100), unreleased, driving both the `encode_key` and the URL-builder tests; fixture 1.2.0 (encoder-mistake rows) and `freshness-headers.json` 1.0.0, vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146), unreleased, every freshness row run through the `GET` response's header reader (`freshness_from_headers`); `file-backend.json` 1.1.0 vendored byte for byte + CI-executed since 0.9.0 ([cachekit-rs#107](https://github.com/cachekit-io/cachekit-rs/pull/107); `.crate` inspected 2026-10-05), by tests in `src/backend/file.rs` behind the `file` feature, which default CI enables; `1.2.0` re-vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158), unreleased, every vector through both header read paths (`read_entry` behind `get`, `probe_header` behind `exists` and `ttl`), the expired flagged entries included, and the five vectors a version-1 writer can produce through `write_entry`; `python-frame.json` first vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158), unreleased, its 16 CK-prefixed vectors refused by both decoders and `get` / `interop_get` / `interop_get_swr`, on a client without encryption (auto mode by the trailing bytes, interop mode by naming the frame) — ⚠️ except `bare_envelope_fed_to_frame_reader`, which decodes¹⁵ | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors — fixture 1.1.0 in [cachekit-ts#143](https://github.com/cachekit-io/cachekit-ts/pull/143), unreleased; fixture 1.2.0 in [cachekit-ts#164](https://github.com/cachekit-io/cachekit-ts/pull/164), unreleased; fixture 1.3.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), unreleased: the key, value and error vectors and both reader groups, every value vector read back through `decodeInteropValue` as its value, with the reader fixes two of them needed (a `__proto__` map key reads as an ordinary key, and a leading U+FEFF is kept); decode bounds enforced ([#112](https://github.com/cachekit-io/cachekit-ts/pull/112)); `decode-bounds.json` vendored + CI-executed since [cachekit-ts#121](https://github.com/cachekit-io/cachekit-ts/pull/121) (LAB-2737), `1.1.0` in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152), unreleased; `1.2.0` (the `incomplete` reason) in [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224), unreleased, both incomplete vectors through all three untrusted decode entry points (`MessagePackSerializer.decode`, `decodeInteropValue`, `deserializeEvent`), asserting the pre-scan's own error; every reject vector asserts the guard error it trips (pre-scan, or the event size cap ahead of it) — ⚠️ except the envelope entry point (`unpack` in `cachekit-core-ts` and `cachekit-core-wasm`), which has no guard to assert until both bindings move to a core release carrying [cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80) and the vectors run through `unpack` (see the Wire format row); for the three reject vectors over the invalidation event entry point's (`deserializeEvent`) 4096 B size cap, the test also calls that site's structural guard (`assertDecodeDepth`, at depth 3) directly and asserts its rejection, as [Decode bounds](spec/interop-mode.md#decode-bounds) requires where a size cap rejects first, in [cachekit-ts#182](https://github.com/cachekit-io/cachekit-ts/pull/182), unreleased; `path-encoding.json` vendored + CI-executed since [cachekit-ts#118](https://github.com/cachekit-io/cachekit-ts/pull/118), unreleased; fixture 1.1.0 (empty-key row) in [cachekit-ts#165](https://github.com/cachekit-io/cachekit-ts/pull/165), unreleased; fixture 1.2.0 (encoder-mistake rows) in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), unreleased, every row through `encodeKey` and all eight request-path operations; `freshness-headers.json` 1.0.0 vendored byte for byte (sha256-pinned) + CI-executed in [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217), unreleased, each row through a real `Headers` and `freshnessFromHeaders`; `wire-format.json` 1.3.0 vendored byte for byte + CI-executed in the Node and Workers lanes in [cachekit-ts#184](https://github.com/cachekit-io/cachekit-ts/pull/184), unreleased (its reject vectors: see the Wire format row); `encryption.json` vendored + CI-executed in the Workers lane since 0.1.4 ([cachekit-ts#78](https://github.com/cachekit-io/cachekit-ts/pull/78)); `1.2.0`, with its `keyring` and `default_tenant` groups, re-vendored byte for byte (sha256-pinned) in [cachekit-ts#211](https://github.com/cachekit-io/cachekit-ts/pull/211), unreleased; `1.3.0` re-vendored byte for byte (sha256-pinned) in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222), unreleased, its accept and eleven hex reject master key input rows driven through `createCache.secure` with the key passed both as `masterKey` and through `CACHEKIT_MASTER_KEY` (cachekit-ts has no raw-key entry point, so the raw-bytes rows do not apply) | ⚠️ Pending | | Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Released — PyPI 0.14.0+¹⁷ ([#220](https://github.com/cachekit-io/cachekit-py/pull/220)) | ✅ Released — crates.io 0.4.0+ ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Released — npm 0.1.3+ ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71)) | ❌ Not implemented | > [!NOTE] > ¹⁴ "N/A" for Rust *auto-mode* key generation means `cachekit-rs` implements no auto-mode key format: `get`/`set` take caller-supplied keys. The `#[cachekit]` macro mints **interop/v1** keys via `interop_key` — required, compile-time-validated `interop = "operation"` and `namespace` attributes, byte-identical across SDKs ([cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35) / LAB-424; keygen itself merged in [#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). The legacy RFC §3.1.5 keygen (`key::generate_cache_key`, `{namespace}:{blake2b256-hex}` — matched no protocol format, and WAS live in every `#[cachekit]` expansion despite the audit's "unused" premise, a proc-macro grep miss) is deleted outright in #35; upgrading is a full cache invalidation for `#[cachekit]` users. `cachekit-core` is a protocol primitive library with no keygen. > -> ¹⁵ Auto-mode **stored bytes** are SDK-internal and differ per SDK — see [wire-format.md → SDK Storage Containers](spec/wire-format.md#sdk-storage-containers-auto-mode). Python stores the ByteStorage envelope *inside* its CK v3 frame; `cachekit-rs` does not use the envelope for values at all (it uses `cachekit-core` only for encryption). Cross-SDK value compatibility is exclusively an [interop-mode](spec/interop-mode.md) property (protocol#11). +> ¹⁵ Auto-mode **stored bytes** are SDK-internal and differ per SDK — see [wire-format.md → SDK Storage Containers](spec/wire-format.md#sdk-storage-containers-auto-mode). Python stores the ByteStorage envelope *inside* its CK v3 frame; `cachekit-rs` does not use the envelope for values at all (it uses `cachekit-core` only for encryption). Because `cachekit-rs` reads plain MessagePack, it decodes another SDK's container that is one well-formed MessagePack document: cachekit-ts's bare ByteStorage envelope (`python-frame.json` `bare_envelope_fed_to_frame_reader`) decodes on every read path of a client without encryption, and a read fails only when the requested type's shape does not fit. That is a WIRE-21 violation for that container, and it holds in every release and on `main`; [`conformance/coverage.md`](conformance/coverage.md) counts cachekit-rs's WIRE-21 vectors as held because they are vendored, not because cachekit-rs refuses them all. crates.io 0.10.0 also decodes a CK frame in auto mode, as the integer 67, a second WIRE-21 violation (its interop mode names the frame); on `main`, auto mode rejects the trailing bytes since [cachekit-rs#140](https://github.com/cachekit-io/cachekit-rs/pull/140), unreleased. Cross-SDK value compatibility is exclusively an [interop-mode](spec/interop-mode.md) property (protocol#11). > -> ¹⁶ "Test vectors in CI" = vectors the SDK's own default CI executes. Beyond the SDKs, this repo's `verify.yml` CI-verifies `interop-mode.json`, `encryption.json`, `python-frame.json`, `file-backend.json` ([`tools/file-backend-reference.py`](tools/file-backend-reference.py)), and — since LAB-423 — `wire-format.json` ([`tools/wire-format-reference.py`](tools/wire-format-reference.py)), and — since LAB-2503 — `decode-bounds.json` ([`tools/decode-bounds-reference.py`](tools/decode-bounds-reference.py), `verify` in both the stdlib and the optional-deps legs), and — since LAB-2879 — `path-encoding.json` ([`tools/path-encoding-verify.py`](tools/path-encoding-verify.py)), and `freshness-headers.json` ([`tools/freshness-headers-verify.py`](tools/freshness-headers-verify.py)) against reference implementations. This repo's `interop-mode.json` is `1.3.0` (44 key, 6 value, 34 error, 6 reader accept and 1 reader reject vectors), which adds key and error vectors for the segment grammar on both segments, out-of-model values nested in a list, a map value or a set, map keys a collator or a length-first sort orders wrongly, a `__proto__` key, strings that start with U+FEFF, integers a float64 cannot hold and small integers passed as `{"$int": …}`, and the two reader groups: documents a value reader must accept though no canonical writer emits them, and a trailing byte it must reject. All three SDKs vendor `1.3.0` byte for byte, in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152) and [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224) (unreleased). This repo's `wire-format.json` is `1.4.0` (LAB-8147): both sides of the `bin8` → `bin16` edge, three constructed envelopes on the `bin16` → `bin32` edge (one a legacy `array32`), eight more `reject_vectors` (step 2's typed decode and pre-scan, an `original_size` with the sign bit set, and a ratio a float32 rounds to exactly 1000), and two new groups: `payload_reject_vectors` and `temporal_sentinel_vectors`. cachekit-py vendors `1.4.0` byte for byte and runs every vector in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) (unreleased), `reject_envelope_slots_overclaim` as a strict expected failure until it pins a `cachekit-core` release with the envelope pre-scan; cachekit-ts vendors `1.3.0`. `1.3.0` added six ByteStorage `reject_vectors` (LAB-6508). cachekit-py runs all six since 0.21.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448)) and cachekit-ts on `main` ([cachekit-ts#198](https://github.com/cachekit-io/cachekit-ts/pull/198), unreleased), each through its envelope read path; `cachekit-rs` stores no envelope¹⁵. No `cachekit-core` release runs them (0.6.0 vendors `wire-format.json` 1.1.0), and no core release has the allocation probe their size-cap and ratio vectors require, so neither SDK asserts that bound yet. This repo's `file-backend.json` is `1.2.0` (LAB-8147), which adds an empty and a binary payload, flag bit 15, reserved values 1 and 255, and two expired entries with an unknown flag or reserved value, which a reader keeps; cachekit-py vendors `1.2.0` byte for byte in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) (unreleased), and cachekit-rs and cachekit-ts vendor `1.1.0`. `python-frame.json` (LAB-8147) adds frame-check boundary vectors, other SDKs' containers and an `encrypted_read_vectors` group; cachekit-py vendors it byte for byte and runs every frame, error and encrypted read vector in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) (unreleased), and no other SDK vendors it. This repo's `path-encoding.json` is `1.2.0` (31 rows, 6 reject); all three SDKs vendor `1.2.0` byte for byte, in [cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522), [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217) and [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146) (unreleased). This repo's `freshness-headers.json` is `1.0.0` (31 rows), and all three SDKs vendor it byte for byte in the same PRs (unreleased). This repo's `encryption.json` is `1.3.0`, which adds master key input rows for [intent-presets.md → Master Key Input](spec/intent-presets.md#master-key-input): a sealed 32-byte key with a leading zero byte, bytes above 7f and every lowercase hex digit, eleven malformed or short hex keys, and five raw keys of the wrong length. cachekit-ts vendors `1.3.0` byte for byte and runs its accept and hex reject rows in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222) (unreleased). This repo's `decode-bounds.json` is `1.2.0` (19 reject, 3 accept), which adds `incomplete_within_slot_budget` and its map twin, documents cut short inside the slot budget, under a new `incomplete` reject reason. All three SDKs vendor `1.2.0` byte for byte, in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152) and [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224) (unreleased). Their previous copies are `1.1.0` (17 reject, 3 accept): cachekit-py's released since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)), cachekit-rs's released since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)), and cachekit-ts's in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152) (unreleased); the SDKs' earlier copies were `1.0.0` revisions (13 reject / 2 accept in py and ts, 10 / 2 in rs). `cachekit-core` vendors `1.1.0` too and drives it through `ByteStorage::retrieve`, on core `main` ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), unreleased. `cache-keys.json` is vendored and CI-verified in cachekit-py since [cachekit-py#229](https://github.com/cachekit-io/cachekit-py/pull/229) (LAB-425). This repo's copy is `1.1.0` (17 vectors; the first 10 byte-identical to the v0.5.0 originals); cachekit-py vendors `1.1.0` byte for byte since 0.21.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)). +> ¹⁶ "Test vectors in CI" = vectors the SDK's own default CI executes. Beyond the SDKs, this repo's `verify.yml` CI-verifies `interop-mode.json`, `encryption.json`, `python-frame.json`, `file-backend.json` ([`tools/file-backend-reference.py`](tools/file-backend-reference.py)), and — since LAB-423 — `wire-format.json` ([`tools/wire-format-reference.py`](tools/wire-format-reference.py)), and — since LAB-2503 — `decode-bounds.json` ([`tools/decode-bounds-reference.py`](tools/decode-bounds-reference.py), `verify` in both the stdlib and the optional-deps legs), and — since LAB-2879 — `path-encoding.json` ([`tools/path-encoding-verify.py`](tools/path-encoding-verify.py)), and `freshness-headers.json` ([`tools/freshness-headers-verify.py`](tools/freshness-headers-verify.py)) against reference implementations. This repo's `interop-mode.json` is `1.3.0` (44 key, 6 value, 34 error, 6 reader accept and 1 reader reject vectors), which adds key and error vectors for the segment grammar on both segments, out-of-model values nested in a list, a map value or a set, map keys a collator or a length-first sort orders wrongly, a `__proto__` key, strings that start with U+FEFF, integers a float64 cannot hold and small integers passed as `{"$int": …}`, and the two reader groups: documents a value reader must accept though no canonical writer emits them, and a trailing byte it must reject. All three SDKs vendor `1.3.0` byte for byte, in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152) and [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224) (unreleased). This repo's `wire-format.json` is `1.4.0` (LAB-8147): both sides of the `bin8` → `bin16` edge, three constructed envelopes on the `bin16` → `bin32` edge (one a legacy `array32`), eight more `reject_vectors` (step 2's typed decode and pre-scan, an `original_size` with the sign bit set, and a ratio a float32 rounds to exactly 1000), and two new groups: `payload_reject_vectors` and `temporal_sentinel_vectors`. cachekit-py vendors `1.4.0` byte for byte and runs every vector in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) (unreleased), `reject_envelope_slots_overclaim` as a strict expected failure until it pins a `cachekit-core` release with the envelope pre-scan; cachekit-ts vendors `1.3.0`. `1.3.0` added six ByteStorage `reject_vectors` (LAB-6508). cachekit-py runs all six since 0.21.0 ([cachekit-py#448](https://github.com/cachekit-io/cachekit-py/pull/448)) and cachekit-ts on `main` ([cachekit-ts#198](https://github.com/cachekit-io/cachekit-ts/pull/198), unreleased), each through its envelope read path; `cachekit-rs` stores no envelope¹⁵. No `cachekit-core` release runs them (0.6.0 vendors `wire-format.json` 1.1.0), and no core release has the allocation probe their size-cap and ratio vectors require, so neither SDK asserts that bound yet. This repo's `file-backend.json` is `1.2.0` (LAB-8147), which adds an empty and a binary payload, flag bit 15, reserved values 1 and 255, and two expired entries with an unknown flag or reserved value, which a reader keeps; cachekit-py vendors `1.2.0` byte for byte in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) and cachekit-rs in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158) (both unreleased), and cachekit-ts vendors `1.1.0`. `python-frame.json` (LAB-8147) adds frame-check boundary vectors, other SDKs' containers and an `encrypted_read_vectors` group; cachekit-py vendors it byte for byte and runs every frame, error and encrypted read vector in [cachekit-py#550](https://github.com/cachekit-io/cachekit-py/pull/550) (unreleased); cachekit-rs vendors it byte for byte in [cachekit-rs#158](https://github.com/cachekit-io/cachekit-rs/pull/158) (unreleased); cachekit-ts does not vendor it. This repo's `path-encoding.json` is `1.2.0` (31 rows, 6 reject); all three SDKs vendor `1.2.0` byte for byte, in [cachekit-py#522](https://github.com/cachekit-io/cachekit-py/pull/522), [cachekit-ts#217](https://github.com/cachekit-io/cachekit-ts/pull/217) and [cachekit-rs#146](https://github.com/cachekit-io/cachekit-rs/pull/146) (unreleased). This repo's `freshness-headers.json` is `1.0.0` (31 rows), and all three SDKs vendor it byte for byte in the same PRs (unreleased). This repo's `encryption.json` is `1.3.0`, which adds master key input rows for [intent-presets.md → Master Key Input](spec/intent-presets.md#master-key-input): a sealed 32-byte key with a leading zero byte, bytes above 7f and every lowercase hex digit, eleven malformed or short hex keys, and five raw keys of the wrong length. cachekit-ts vendors `1.3.0` byte for byte and runs its accept and hex reject rows in [cachekit-ts#222](https://github.com/cachekit-io/cachekit-ts/pull/222) (unreleased). This repo's `decode-bounds.json` is `1.2.0` (19 reject, 3 accept), which adds `incomplete_within_slot_budget` and its map twin, documents cut short inside the slot budget, under a new `incomplete` reject reason. All three SDKs vendor `1.2.0` byte for byte, in [cachekit-py#537](https://github.com/cachekit-io/cachekit-py/pull/537), [cachekit-rs#152](https://github.com/cachekit-io/cachekit-rs/pull/152) and [cachekit-ts#224](https://github.com/cachekit-io/cachekit-ts/pull/224) (unreleased). Their previous copies are `1.1.0` (17 reject, 3 accept): cachekit-py's released since 0.20.0 ([cachekit-py#363](https://github.com/cachekit-io/cachekit-py/pull/363)), cachekit-rs's released since 0.8.0 ([cachekit-rs#93](https://github.com/cachekit-io/cachekit-rs/pull/93)), and cachekit-ts's in [cachekit-ts#152](https://github.com/cachekit-io/cachekit-ts/pull/152) (unreleased); the SDKs' earlier copies were `1.0.0` revisions (13 reject / 2 accept in py and ts, 10 / 2 in rs). `cachekit-core` vendors `1.1.0` too and drives it through `ByteStorage::retrieve`, on core `main` ([cachekit-core#80](https://github.com/cachekit-io/cachekit-core/pull/80)), unreleased. `cache-keys.json` is vendored and CI-verified in cachekit-py since [cachekit-py#229](https://github.com/cachekit-io/cachekit-py/pull/229) (LAB-425). This repo's copy is `1.1.0` (17 vectors; the first 10 byte-identical to the v0.5.0 originals); cachekit-py vendors `1.1.0` byte for byte since 0.21.0 ([cachekit-py#460](https://github.com/cachekit-io/cachekit-py/pull/460)). > > ¹⁷ Version cells are **floors** (`X+`), not snapshots — they stay true as new versions publish; check the registry for the current release. Python's floor is the first *installable* one: interop merged under the `v0.13.0` tag, but neither `0.12.0` nor `0.13.0` was ever published to PyPI, so `0.14.0` is the earliest PyPI release containing interop mode. Do not "correct" this to 0.13.0 from the cachekit-py changelog alone.