From 9f6ef06c73cff378c30be388425d0e5ea700756d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 01:05:02 +1000 Subject: [PATCH 1/6] docs(matrix): correct backend rows + add backend-abstraction section (LAB-273 audit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend-parity audit findings, verified against code (py 0.12.0, rs 0.3.0, ts 0.1.2): - DynamoDB ships in NO SDK. The Python ✅ traced to the custom-backend tutorial (docs/backends/custom.md) — an extension-point example, not shipped support. Row corrected to ❌ across the board. - TS Cloudflare Workers cell N/A → ❌: Workers is a plausible target for TS but blocked by NAPI-native crypto and ioredis; N/A misstates it. - New 'Backend Abstraction' section: required interface per SDK (health required py/rs, absent ts; close()+keyPrefix ts-only), optional capability interfaces, per-backend implementation coverage, and the lock-API shape divergence (py context-manager vs rs/ts lock_id token). - Reliability rows qualified per backend: Python locking is Redis+SaaS (not SaaS-only); rs/ts locking SaaS-only; TS TTL inspection SaaS-only. - Lock-id header migration (CWE-532) recorded complete in all three SDKs: py#131 closed, rs#24 closed, ts ships the header (ts#63 open only for an unrelated NAPI-rebuild item). - Backend-selection note: Python is the only SDK with env backend auto-detection; rs/ts env vars are SaaS-credential-only. Recorded as a finding, not a prescription. - py version 0.11.1 → 0.12.0 (v0.12.0 tag exists). Co-authored-by: multica-agent --- sdk-feature-matrix.md | 61 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 05c5fce..df8b48c 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-07-20 — LAB-381 stale-while-revalidate (server stale-grace) specified in [spec/saas-api.md](spec/saas-api.md#stale-while-revalidate)* +*Last updated: 2026-07-21 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* @@ -18,6 +18,7 @@ - [Core Features](#core-features) - [Encryption](#encryption) - [Cache Backends](#cache-backends) +- [Backend Abstraction](#backend-abstraction) - [Reliability Features](#reliability-features) - [Developer Experience](#developer-experience) - [Protocol Compliance](#protocol-compliance) @@ -29,7 +30,7 @@ | SDK | Package | Version | Language | Status | | :--- | :--- | :---: | :--- | :---: | -| cachekit-py | `cachekit` (PyPI) | 0.11.1 | Python 3.10+ | ✅ Production | +| cachekit-py | `cachekit` (PyPI) | 0.12.0 | Python 3.10+ | ✅ Production | | cachekit-rs | `cachekit-rs` (crates.io) | 0.3.0 | Rust 1.82+ | ✅ Production | | cachekit-core | `cachekit-core` (crates.io) | 0.3.0 | Rust (shared core) | ✅ Production | | cachekit-ts | `@cachekit-io/cachekit` (npm) | 0.1.2 | TypeScript | ✅ Production | @@ -70,12 +71,48 @@ | Backend | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Redis (direct) | ✅ | ✅ via fred | ✅ | ❌ | -| Memcached | ✅ (env auto-detect) | ❌ | ❌ | ❌ | -| File (local) | ✅ (env auto-detect) | ❌ | ❌ | ❌ | -| CacheKit SaaS (HTTP) | ✅ | ✅ reqwest (native) + fetch (Workers) | ✅ | 🔜 Planned | -| Cloudflare Workers | N/A | ✅ `workers` feature | N/A | N/A | -| DynamoDB | ✅ | ❌ | ❌ | ❌ | +| Redis (direct) | ✅ `backends/redis` (redis-py, required dep) | ✅ `redis` feature (fred) | ✅ `backends/redis.ts` (ioredis) | ❌ | +| Memcached | ✅ `backends/memcached` (`memcached` extra) | ❌ | ❌ | ❌ | +| File (local) | ✅ `backends/file` (stdlib + mmap) | ❌ | ❌ | ❌ | +| CacheKit SaaS (HTTP) | ✅ `backends/cachekitio` (httpx) | ✅ `cachekitio` feature (reqwest, default) | ✅ `backends/cachekitio.ts` (fetch) | 🔜 Planned | +| Cloudflare Workers | N/A | ✅ `workers` feature (`worker::Fetch`) | ❌ Node 20+ only¹ | N/A | +| DynamoDB | ❌² | ❌ | ❌ | ❌ | + +> ¹ The TS SDK cannot run on the Workers runtime today: crypto is a native NAPI module and the Redis backend is ioredis (TCP). Recorded as ❌ (a plausible target, unsupported) rather than N/A. +> +> ² DynamoDB has never shipped in any SDK. The previous Python ✅ traced to the [custom-backend tutorial](https://github.com/cachekit-io/cachekit-py/blob/main/docs/backends/custom.md), which shows how a *user* can implement the backend protocol against DynamoDB — that is an extension point, not shipped support (LAB-273). + +**Backend selection / env auto-detection:** Python is the only SDK that auto-detects *which* backend to use from the environment — a single unambiguous selector (`CACHEKIT_API_KEY` → SaaS, `CACHEKIT_REDIS_URL` → Redis, `CACHEKIT_MEMCACHED_SERVERS` → Memcached, `CACHEKIT_FILE_CACHE_DIR` → File; fallback `REDIS_URL` / localhost Redis; setting more than one selector raises `ConfigurationError`). Rust `CacheKit::from_env()` reads SaaS credentials only (`CACHEKIT_API_KEY` / `CACHEKIT_API_URL` / `CACHEKIT_MASTER_KEY` / `CACHEKIT_DEFAULT_TTL`) and always builds the CachekitIO backend — Redis is wired explicitly via `.backend()`. TypeScript has no backend auto-detection: each preset fixes the backend type and env vars (`CACHEKIT_API_KEY`, `CACHEKIT_MASTER_KEY`) serve only as credential fallbacks. Whether rs/ts should gain selector-style auto-detection is an open product question, not a recorded decision (LAB-273 finding). + +--- + +## Backend Abstraction + +The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-21 (LAB-273). + +### Required interface + +| Aspect | Python (`BaseBackend`) | Rust (`Backend`) | TypeScript (`Backend`) | +| :--- | :--- | :--- | :--- | +| Form | PEP 544 structural protocol, `@runtime_checkable` | `#[async_trait]`, `Send + Sync` (auto `?Send` on wasm32 / `unsync` feature) | interface (structural) | +| Core ops | `get` / `set` / `delete` / `exists` | `get` / `set` / `delete` / `exists` | `get` / `set` / `delete` / `exists` | +| Health check | ✅ `health_check() -> (bool, details)` required | ✅ `health() -> HealthStatus` required | ❌ not in the interface (SaaS backend has an internal check) | +| Lifecycle | — | — | ✅ `close()` required | +| Interop key-prefix guard | — | — | ✅ optional `readonly keyPrefix` (interop mode fails closed on hidden prefixes) | + +### Optional capabilities — and which shipped backends implement them + +| Capability | Python | Rust | TypeScript | +| :--- | :--- | :--- | :--- | +| TTL inspect / refresh | `TTLInspectableBackend` — Redis ✅, SaaS ✅, Memcached/File ❌ | `TtlInspectable` — Redis ✅, SaaS ✅, Workers ❌ | `TTLBackend` — SaaS ✅ (`TTLCachekitIO`), Redis ❌ | +| Distributed locking | `LockableBackend` — Redis ✅ (`redis.lock.Lock`), SaaS ✅ | `LockableBackend` — SaaS ✅, Redis ❌, Workers ❌ | `LockableBackend` — SaaS ✅ (`LockableCachekitIO`), Redis ❌ | +| Per-operation timeout | `TimeoutConfigurableBackend` — Redis ✅ (SaaS ships a non-protocol `with_timeout` variant) | — no equivalent | — no equivalent | +| Zero-copy buffer read | `BufferReadableBackend` / `BufferHandle` — File ✅ (mmap; #171) | — no equivalent | — no equivalent | + +> [!NOTE] +> **Lock API shape divergence:** Python's `acquire_lock` is an async context manager yielding `bool` — the lock token stays internal and release is automatic. Rust and TypeScript return the raw `lock_id` capability token from `acquire_lock`/`acquireLock` and require an explicit `release_lock(key, lock_id)` — a direct mirror of the SaaS lock endpoint. All three pass the **bare cache key** (backends own any `:lock` namespace derivation). Porting a lockable backend across SDKs must bridge this shape difference. +> +> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but only Python implements the optional capabilities on Redis. Rust's Redis backend lacks locking; TypeScript's Redis backend lacks both locking and TTL inspection; Rust's Workers backend lacks both despite speaking the same SaaS API (gap tickets under LAB-102). --- @@ -85,13 +122,13 @@ | :--- | :---: | :---: | :---: | :---: | | Circuit breaker | ✅ | ❌ | ✅ | ❌ | | Backpressure | ✅ | ❌ | ⚠️ Concurrent refresh limits | ❌ | -| Distributed locking | ✅ | ✅ SaaS backend (`LockableBackend`) | ✅ SaaS backend only | ❌ | +| Distributed locking | ✅ Redis + SaaS backends | ✅ SaaS backend only (`LockableBackend`; Redis/Workers lack impls) | ✅ SaaS backend only | ❌ | | L1/L2 dual-layer cache | ✅ | ✅ moka (native) / `l1` feature | ✅ | ❌ | | Cache stampede prevention | ✅ | ❌ | ✅ Version tokens + background L1 refresh | ❌ | -| TTL management | ✅ | ✅ `TtlInspectable` trait | ✅ | ❌ | +| TTL management | ✅ Redis + SaaS | ✅ Redis + SaaS (`TtlInspectable`; Workers ❌) | ✅ SaaS only (`TTLBackend`) | ❌ | | Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ | -> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). SaaS dual-reads both during the rollout; SDKs migrate to header-only — Python (#131), TypeScript (#63), Rust (#24). See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). +> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-21, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). --- @@ -154,7 +191,7 @@ its spec: - Hybrid Python-Rust architecture: decorators and orchestration in Python, ByteStorage and encryption in Rust (via PyO3) - The `cachekit-core` Rust crate is the canonical implementation for compression, checksums, and encryption - 4 serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON), Arrow (columnar) -- Backends: Redis, Memcached, File (local), CacheKit SaaS — Memcached and File auto-detected from environment +- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from a single unambiguous env selector (see [Cache Backends](#cache-backends)) - Config via pydantic-settings; secrets via `SecretStr` From f0ae021f659d283aaafd2a03ad3958cf58aaa609 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 01:06:04 +1000 Subject: [PATCH 2/6] docs: correct SDK feature matrix to code-verified protocol-adherence reality (LAB-272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit of py/rs/ts main branches against the four protocol requirements: - Interop mode: merged in cachekit-py (#220) and cachekit-ts (#71), in review for cachekit-rs (#33) — the '❌ Not implemented' row was stale. - Test vectors row now states what each SDK's CI actually executes; wire-format.json and cache-keys.json are verified by no CI anywhere (LAB-423, LAB-425). - Rust keygen cell: caller-supplied keys remain N/A, but the crate exports an unused legacy generate_cache_key matching no protocol format (LAB-424); conformant keygen arrives with cachekit-rs#33. - AAD v0x03 cells record component arity per SDK (py auto serializers emit the optional fifth original_type component; rs/ts/interop are four-component) — byte-identity code-verified per protocol#12. - cachekit-py fleet version 0.11.1 -> 0.12.0. Co-authored-by: multica-agent --- sdk-feature-matrix.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 05c5fce..80f415f 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-07-20 — LAB-381 stale-while-revalidate (server stale-grace) specified in [spec/saas-api.md](spec/saas-api.md#stale-while-revalidate)* +*Last updated: 2026-07-21 — LAB-272 code-verified protocol-adherence audit: interop/v1 merged in Python ([cachekit-py#220](https://github.com/cachekit-io/cachekit-py/pull/220)) and TypeScript ([cachekit-ts#71](https://github.com/cachekit-io/cachekit-ts/pull/71)), Rust in review ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)); test-vector CI coverage corrected* @@ -29,7 +29,7 @@ | SDK | Package | Version | Language | Status | | :--- | :--- | :---: | :--- | :---: | -| cachekit-py | `cachekit` (PyPI) | 0.11.1 | Python 3.10+ | ✅ Production | +| cachekit-py | `cachekit` (PyPI) | 0.12.0 | Python 3.10+ | ✅ Production | | cachekit-rs | `cachekit-rs` (crates.io) | 0.3.0 | Rust 1.82+ | ✅ Production | | cachekit-core | `cachekit-core` (crates.io) | 0.3.0 | Rust (shared core) | ✅ Production | | cachekit-ts | `@cachekit-io/cachekit` (npm) | 0.1.2 | TypeScript | ✅ Production | @@ -62,7 +62,7 @@ | Counter-based nonces | ✅ via Rust | ✅ | ✅ via NAPI (Rust) | ❌ use random | > [!IMPORTANT] -> AAD v0x03 is required for protocol compliance. SDKs without it cannot safely interoperate with encrypted payloads from compliant SDKs — the auth tag will fail verification. See [spec/encryption.md](spec/encryption.md#additional-authenticated-data-aad). Python, Rust, and TypeScript SDKs are compliant as of 2026-04-06. +> AAD v0x03 is required for protocol compliance. SDKs without it cannot safely interoperate with encrypted payloads from compliant SDKs — the auth tag will fail verification. See [spec/encryption.md](spec/encryption.md#additional-authenticated-data-aad). Python, Rust, and TypeScript construction was code-verified byte-identical on 2026-07-21 (LAB-272): identical `[0x03][4-byte BE length][component]` layout with the frozen `True`/`False` compressed tokens (protocol#12). Python's auto serializers append the optional `original_type` fifth component; Rust, TypeScript, and interop mode always emit exactly four. Python's read path fails closed when encryption is enabled but a stored entry claims plaintext ([cachekit-py#215](https://github.com/cachekit-io/cachekit-py/pull/215)). --- @@ -129,20 +129,23 @@ its spec: | Requirement | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Key generation (Blake2b) | ✅ Compliant | N/A (SDK-level, not in core) | ✅ Compliant | ⚠️ Untested | +| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (caller-supplied keys)² — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | | Wire format (ByteStorage) | ✅ Compliant¹ | ✅ Canonical (`cachekit-core`) — unused for stored values¹ | ✅ Compliant | ⚠️ Untested | | Storage container (auto mode)¹ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | | Encryption (AES-256-GCM) | ✅ Compliant | ✅ Canonical (cachekit-core) | ✅ Compliant | ⚠️ Untested | -| AAD v0x03 | ✅ Compliant | ✅ Compliant | ✅ Compliant | ❌ Not implemented | +| AAD v0x03 | ✅ Compliant (4 components; +`original_type` 5th on auto serializers) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | | SaaS API | ✅ Compliant | ✅ Compliant (CachekitIO backend) | ✅ Compliant | ❌ Not implemented | -| Test vectors | ⚠️ Pending | ⚠️ Pending | ✅ Python cross-SDK vectors | ⚠️ Pending | -| Interop mode ([spec](spec/interop-mode.md), opt-in) | ❌ Not implemented | ❌ Not implemented | ❌ Not implemented | ❌ Not implemented | +| Test vectors in CI³ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) | ❌ None on `main` — interop vectors land with [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ interop/v1 (full set) + inline Python-generated key/AAD vectors | ⚠️ Pending | +| Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Merged ([#220](https://github.com/cachekit-io/cachekit-py/pull/220), unreleased) | 🚧 In review ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Merged ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71), unreleased) | ❌ Not implemented | > [!NOTE] -> "N/A" for Rust key generation means `cachekit-core` is a protocol primitive library. Key generation (Blake2b) is an SDK-level concern — `cachekit-rs` delegates cache key construction to the caller via the `key` parameter on `get`/`set`/`#[cachekit]`. +> ¹ 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). > [!NOTE] -> ¹ 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). +> ² "N/A" for Rust key generation means auto-mode `cachekit-rs` delegates cache key construction to the caller via the `key` parameter on `get`/`set`/`#[cachekit]`; `cachekit-core` is a protocol primitive library with no keygen. The crate does export an unused legacy `key::generate_cache_key` that matches **no** protocol key format — do not use it for cross-SDK keys (removal/replacement tracked as LAB-424). Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). + +> [!NOTE] +> ³ "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`, and `python-frame.json` against reference implementations. **Not verified by any CI anywhere**: `wire-format.json` (tracked as LAB-423) and `cache-keys.json` (generated by cachekit-py v0.5.0; tracked as LAB-425). --- From fa218079cb96170953b4ff3a43dc1377332ff869 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 01:42:03 +1000 Subject: [PATCH 3/6] =?UTF-8?q?docs(matrix):=20correct=20Rust=20keygen=20f?= =?UTF-8?q?ootnote=20=E2=80=94=20#[cachekit]=20has=20no=20key=20param=20an?= =?UTF-8?q?d=20generate=5Fcache=5Fkey=20is=20not=20unused=20[LAB-424]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The footnote claimed cachekit-rs delegates key construction via a 'key' parameter on #[cachekit] (no such parameter exists) and called key::generate_cache_key 'unused' (it is called at runtime by every #[cachekit] expansion — proc-macro call sites are invisible to grep). Rewritten to the verified posture: get/set caller-supplied; macro derives an SDK-internal legacy RFC-shape key, made private in cachekit-rs#35 (LAB-424); conformant keygen arrives with #33. Co-authored-by: multica-agent --- sdk-feature-matrix.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 80f415f..c8a45a7 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -129,7 +129,7 @@ its spec: | Requirement | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (caller-supplied keys)² — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | +| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (no spec'd keygen)² — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | | Wire format (ByteStorage) | ✅ Compliant¹ | ✅ Canonical (`cachekit-core`) — unused for stored values¹ | ✅ Compliant | ⚠️ Untested | | Storage container (auto mode)¹ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | | Encryption (AES-256-GCM) | ✅ Compliant | ✅ Canonical (cachekit-core) | ✅ Compliant | ⚠️ Untested | @@ -142,7 +142,7 @@ its spec: > ¹ 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). > [!NOTE] -> ² "N/A" for Rust key generation means auto-mode `cachekit-rs` delegates cache key construction to the caller via the `key` parameter on `get`/`set`/`#[cachekit]`; `cachekit-core` is a protocol primitive library with no keygen. The crate does export an unused legacy `key::generate_cache_key` that matches **no** protocol key format — do not use it for cross-SDK keys (removal/replacement tracked as LAB-424). Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). +> ² "N/A" for Rust key generation means `cachekit-rs` implements no spec'd key format: `get`/`set` take caller-supplied keys, and the `#[cachekit]` macro (which has **no** `key` parameter) derives an SDK-internal legacy key — `{namespace}:{blake2b256-hex}` over the *unqualified* function name, the obsolete RFC §3.1.5 shape matching **no** current protocol key format, never usable cross-SDK. That derivation is macro-internal plumbing (it IS called at runtime by every `#[cachekit]` expansion — the LAB-424 "unused" premise was a grep miss), removed from the public API in [cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35). `cachekit-core` is a protocol primitive library with no keygen. Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). > [!NOTE] > ³ "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`, and `python-frame.json` against reference implementations. **Not verified by any CI anywhere**: `wire-format.json` (tracked as LAB-423) and `cache-keys.json` (generated by cachekit-py v0.5.0; tracked as LAB-425). From 7e8e73a12abf8e2d6b1749bc4885dfb86fcf7e8d Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 04:27:20 +1000 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20coderabbit=20review=20?= =?UTF-8?q?=E2=80=94=20matrix=20date/coverage/backend-selection=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Correct future-dated audit stamps (2026-07-21 → 2026-07-20) in the Last-updated banner, Backend Abstraction audit note, and lock-id migration note. - Fix Redis optional-capability summary to match the capability table: Python has both locking + TTL inspection, Rust Redis has TTL inspection only, TS Redis neither. - Restore the documented Python backend-selection fallbacks (REDIS_URL/localhost, ConfigurationError on ambiguous selectors) in the SDK summary bullet. CodeRabbit-Resolved: sdk-feature-matrix.md:9:Remove future-dated completion claims CodeRabbit-Resolved: sdk-feature-matrix.md:115:Correct the Redis capability summary CodeRabbit-Resolved: sdk-feature-matrix.md:194:Preserve backend-selection fallbacks --- sdk-feature-matrix.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index df8b48c..0e10e27 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -6,7 +6,7 @@ **Feature parity and compliance status across all CacheKit SDK implementations.** -*Last updated: 2026-07-21 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* +*Last updated: 2026-07-20 — LAB-273 backend-parity audit: DynamoDB corrected (ships nowhere), backend-abstraction section added, locking/TTL rows qualified per backend, lock-id header migration recorded complete* @@ -88,7 +88,7 @@ ## Backend Abstraction -The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-21 (LAB-273). +The contract a storage backend must satisfy per SDK (bytes in / bytes out; serialization and encryption live above the backend). Audited against code 2026-07-20 (LAB-273). ### Required interface @@ -112,7 +112,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria > [!NOTE] > **Lock API shape divergence:** Python's `acquire_lock` is an async context manager yielding `bool` — the lock token stays internal and release is automatic. Rust and TypeScript return the raw `lock_id` capability token from `acquire_lock`/`acquireLock` and require an explicit `release_lock(key, lock_id)` — a direct mirror of the SaaS lock endpoint. All three pass the **bare cache key** (backends own any `:lock` namespace derivation). Porting a lockable backend across SDKs must bridge this shape difference. > -> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but only Python implements the optional capabilities on Redis. Rust's Redis backend lacks locking; TypeScript's Redis backend lacks both locking and TTL inspection; Rust's Workers backend lacks both despite speaking the same SaaS API (gap tickets under LAB-102). +> **Coverage, not shape, is the parity gap:** all three SDKs use the same required-base + optional-capability pattern, but Redis optional-capability coverage varies by SDK: Python has the broadest coverage (both locking and TTL inspection), Rust's Redis backend implements TTL inspection only (no locking), and TypeScript's Redis backend implements neither. Rust's Workers backend also lacks both despite speaking the same SaaS API (gap tickets under LAB-102). --- @@ -128,7 +128,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria | TTL management | ✅ Redis + SaaS | ✅ Redis + SaaS (`TtlInspectable`; Workers ❌) | ✅ SaaS only (`TTLBackend`) | ❌ | | Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ | -> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-21, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). +> **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-20, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). --- @@ -191,7 +191,7 @@ its spec: - Hybrid Python-Rust architecture: decorators and orchestration in Python, ByteStorage and encryption in Rust (via PyO3) - The `cachekit-core` Rust crate is the canonical implementation for compression, checksums, and encryption - 4 serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON), Arrow (columnar) -- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from a single unambiguous env selector (see [Cache Backends](#cache-backends)) +- Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from the environment: a single unambiguous selector, with `REDIS_URL`/localhost fallback, and ambiguous (multiple) selectors raising `ConfigurationError` (see [Cache Backends](#cache-backends)) - Config via pydantic-settings; secrets via `SecretStr` From 8f616e9b62996ee1057df1ee536eac03038f7d08 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 21 Jul 2026 11:22:38 +1000 Subject: [PATCH 5/6] =?UTF-8?q?docs(matrix):=20presentation-clarity=20fixe?= =?UTF-8?q?s=20=E2=80=94=20Python=20AAD=20norm,=20footnote=20order,=20TS?= =?UTF-8?q?=20vector=20wording=20[LAB-445]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four clarity nits flagged by the LAB-272 expert-panel review of #24; no factual claim changes (verdicts, marks, versions, byte constants preserved): - Python AAD cell: 5-component is the Python norm (every auto serializer appends original_type); interop mode is the sole 4-component path — matches the line-65 callout framing instead of reading like an edge case. - Footnotes renumbered so first appearance runs 1, 2, 3 in reading order (keygen note was 2 but appeared before 1); note blocks reordered to match. - TS test-vectors cell: 'key/AAD' disambiguated to what CI actually runs — inline Python-generated AAD-construction + encryption (decrypt-Python- ciphertext) vectors. 'key' dropped: key-generation.protocol.test.ts holds property checks only, no Python-generated key values; key-vector coverage is the interop/v1 set (33 key vectors), now credited explicitly. - AAD byte-layout detail deduped out of the matrix callout: layout, frozen True/False tokens, and the protocol#12 decision are already normative in spec/encryption.md, which stays linked — matrix keeps parity altitude. Co-authored-by: multica-agent --- sdk-feature-matrix.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index c8a45a7..149753d 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -62,7 +62,7 @@ | Counter-based nonces | ✅ via Rust | ✅ | ✅ via NAPI (Rust) | ❌ use random | > [!IMPORTANT] -> AAD v0x03 is required for protocol compliance. SDKs without it cannot safely interoperate with encrypted payloads from compliant SDKs — the auth tag will fail verification. See [spec/encryption.md](spec/encryption.md#additional-authenticated-data-aad). Python, Rust, and TypeScript construction was code-verified byte-identical on 2026-07-21 (LAB-272): identical `[0x03][4-byte BE length][component]` layout with the frozen `True`/`False` compressed tokens (protocol#12). Python's auto serializers append the optional `original_type` fifth component; Rust, TypeScript, and interop mode always emit exactly four. Python's read path fails closed when encryption is enabled but a stored entry claims plaintext ([cachekit-py#215](https://github.com/cachekit-io/cachekit-py/pull/215)). +> AAD v0x03 is required for protocol compliance. SDKs without it cannot safely interoperate with encrypted payloads from compliant SDKs — the auth tag will fail verification. Python, Rust, and TypeScript construction was code-verified byte-identical on 2026-07-21 (LAB-272); the normative byte layout and the frozen `True`/`False` compressed tokens (protocol#12) are defined in [spec/encryption.md](spec/encryption.md#additional-authenticated-data-aad). Python's auto serializers append the optional `original_type` fifth component; Rust, TypeScript, and interop mode always emit exactly four. Python's read path fails closed when encryption is enabled but a stored entry claims plaintext ([cachekit-py#215](https://github.com/cachekit-io/cachekit-py/pull/215)). --- @@ -129,20 +129,20 @@ its spec: | Requirement | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (no spec'd keygen)² — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | -| Wire format (ByteStorage) | ✅ Compliant¹ | ✅ Canonical (`cachekit-core`) — unused for stored values¹ | ✅ Compliant | ⚠️ Untested | -| Storage container (auto mode)¹ | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | +| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (no spec'd keygen)¹ — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | +| Wire format (ByteStorage) | ✅ Compliant² | ✅ Canonical (`cachekit-core`) — unused for stored values² | ✅ Compliant | ⚠️ Untested | +| Storage container (auto mode)² | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | | Encryption (AES-256-GCM) | ✅ Compliant | ✅ Canonical (cachekit-core) | ✅ Compliant | ⚠️ Untested | -| AAD v0x03 | ✅ Compliant (4 components; +`original_type` 5th on auto serializers) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | +| AAD v0x03 | ✅ Compliant (5 components — every auto serializer appends `original_type`; interop mode is the sole 4-component path) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | | SaaS API | ✅ Compliant | ✅ Compliant (CachekitIO backend) | ✅ Compliant | ❌ Not implemented | -| Test vectors in CI³ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) | ❌ None on `main` — interop vectors land with [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ interop/v1 (full set) + inline Python-generated key/AAD vectors | ⚠️ Pending | +| Test vectors in CI³ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) | ❌ None on `main` — interop vectors land with [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors | ⚠️ Pending | | Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Merged ([#220](https://github.com/cachekit-io/cachekit-py/pull/220), unreleased) | 🚧 In review ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Merged ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71), unreleased) | ❌ Not implemented | > [!NOTE] -> ¹ 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). +> ¹ "N/A" for Rust key generation means `cachekit-rs` implements no spec'd key format: `get`/`set` take caller-supplied keys, and the `#[cachekit]` macro (which has **no** `key` parameter) derives an SDK-internal legacy key — `{namespace}:{blake2b256-hex}` over the *unqualified* function name, the obsolete RFC §3.1.5 shape matching **no** current protocol key format, never usable cross-SDK. That derivation is macro-internal plumbing (it IS called at runtime by every `#[cachekit]` expansion — the LAB-424 "unused" premise was a grep miss), removed from the public API in [cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35). `cachekit-core` is a protocol primitive library with no keygen. Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). > [!NOTE] -> ² "N/A" for Rust key generation means `cachekit-rs` implements no spec'd key format: `get`/`set` take caller-supplied keys, and the `#[cachekit]` macro (which has **no** `key` parameter) derives an SDK-internal legacy key — `{namespace}:{blake2b256-hex}` over the *unqualified* function name, the obsolete RFC §3.1.5 shape matching **no** current protocol key format, never usable cross-SDK. That derivation is macro-internal plumbing (it IS called at runtime by every `#[cachekit]` expansion — the LAB-424 "unused" premise was a grep miss), removed from the public API in [cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35). `cachekit-core` is a protocol primitive library with no keygen. Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). +> ² 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). > [!NOTE] > ³ "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`, and `python-frame.json` against reference implementations. **Not verified by any CI anywhere**: `wire-format.json` (tracked as LAB-423) and `cache-keys.json` (generated by cachekit-py v0.5.0; tracked as LAB-425). From c8824b18fc811cba6568516eb9e351c5fdde8751 Mon Sep 17 00:00:00 2001 From: multica-agent Date: Tue, 21 Jul 2026 23:35:52 +1000 Subject: [PATCH 6/6] =?UTF-8?q?docs(matrix):=20cross-SDK=20feature-gap=20s?= =?UTF-8?q?ynthesis=20=E2=80=94=20code-verified=20refresh=20[LAB-275]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds in the LAB-272 (protocol adherence) and LAB-273 (backend parity) audit branches and corrects every remaining capability row against py/rs/ts main as of 2026-07-21. Trust-bug corrections (cells that claimed code that doesn't run): - Key rotation: was py/rs ✅ — ships in NO SDK. core rotate_key() is a NotImplemented stub; py's KeyRotationState PyO3 binding has zero Python callers (detection-only); ts monitors nonce exhaustion only. Now ❌ fleet-wide with evidence footnote (LAB-516). - Hardware-accel detection: rs ✅ → ⚠️ core-internal (never surfaced); ts N/A → ❌ (same Rust core, nothing exposed) (LAB-523). - TS Arrow: 🔜 Planned → ❌ — zero code/stubs/tracking behind the claim (LAB-524). - TS stampede: ✅ → ⚠️ refresh-path only; no cold-miss single-flight, LockableBackend unwired (LAB-519). - Py client SWR: live in L1-only mode (ObjectCache) — backed-mode SWR stays dead code (LAB-388); split client/server SWR rows (ts client SWR was under-claimed as ❌, it is live). New coverage: Orjson serializer row, Retry / Graceful degradation / Cross-instance invalidation rows (py invalidation package is built but never wired — LAB-520), and an Observability section (ts metrics option silently ignored — LAB-517; rs has no logging/tracing and telemetry headers report disabled by default — LAB-521). Freshness: py 0.13.0 (interop released), rs#33 merged (interop keygen + vectors in cargo CI), MSRV 1.85, core dep 0.3, Node 22+, wire-format + cache-keys vector CI holes recorded closed (core#55 / protocol#26). Co-authored-by: multica-agent --- sdk-feature-matrix.md | 87 +++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 24 deletions(-) diff --git a/sdk-feature-matrix.md b/sdk-feature-matrix.md index 17456e1..4880682 100644 --- a/sdk-feature-matrix.md +++ b/sdk-feature-matrix.md @@ -20,6 +20,7 @@ - [Cache Backends](#cache-backends) - [Backend Abstraction](#backend-abstraction) - [Reliability Features](#reliability-features) +- [Observability](#observability) - [Developer Experience](#developer-experience) - [Protocol Compliance](#protocol-compliance) - [Architecture Notes](#architecture-notes) @@ -30,8 +31,8 @@ | SDK | Package | Version | Language | Status | | :--- | :--- | :---: | :--- | :---: | -| cachekit-py | `cachekit` (PyPI) | 0.12.0 | Python 3.10+ | ✅ Production | -| cachekit-rs | `cachekit-rs` (crates.io) | 0.3.0 | Rust 1.82+ | ✅ Production | +| cachekit-py | `cachekit` (PyPI) | 0.13.0 | Python 3.10+ | ✅ Production | +| cachekit-rs | `cachekit-rs` (crates.io) | 0.3.0 | Rust 1.85+ | ✅ Production | | cachekit-core | `cachekit-core` (crates.io) | 0.3.0 | Rust (shared core) | ✅ Production | | cachekit-ts | `@cachekit-io/cachekit` (npm) | 0.1.2 | TypeScript | ✅ Production | | cachekit-php | — | — | PHP 8.1+ | 🔜 Development | @@ -42,11 +43,14 @@ | Feature | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| StandardSerializer (MessagePack) | ✅ | ✅ via rmp-serde | ✅ | 🔜 Planned | +| StandardSerializer (MessagePack) | ✅ | ✅ via rmp-serde | ✅ via @msgpack/msgpack | 🔜 Planned | | AutoSerializer (Python-specific) | ✅ | N/A | N/A | N/A | -| ArrowSerializer (columnar) | ✅ | N/A | 🔜 Planned | ❌ | +| OrjsonSerializer (fast JSON) | ✅ `[json]` extra | N/A | N/A | N/A | +| ArrowSerializer (columnar) | ✅ `[data]` extra | N/A | ❌ (LAB-524)¹ | ❌ | | ByteStorage (LZ4 + xxHash3-64) | ✅ via Rust FFI | ✅ canonical (cachekit-core) | ✅ via NAPI (Rust) | 🔜 Planned | -| Blake2b-256 key generation | ✅ | N/A | ✅ via @noble/hashes | 🔜 Planned | +| Blake2b-256 key generation | ✅ | ✅ interop mode ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) / N/A auto mode | ✅ via @noble/hashes | 🔜 Planned | + +> ¹ TS Arrow was listed 🔜 Planned since 2026-06-06 with zero code, stubs, or tracking behind it (LAB-275 audit). Corrected to ❌; LAB-524 owns the implement-or-decline decision. Orjson is a Python-ecosystem performance serializer (N/A elsewhere by design — Rust and TS serialize natively to MessagePack). --- @@ -58,13 +62,17 @@ | HKDF-SHA256 key derivation | ✅ via Rust FFI | ✅ | ✅ via NAPI (Rust) | 🔜 Planned | | Per-tenant key isolation | ✅ | ✅ | ✅ via TenantKeys NAPI | 🔜 Planned | | AAD v0x03 (cache_key binding) | ✅ | ✅ | ✅ | ❌ | -| Key rotation | ✅ | ✅ | ❌ | ❌ | -| Hardware acceleration detection | ✅ | ✅ | N/A | N/A | +| Key rotation | ❌ detection only¹ | ❌¹ | ❌ nonce monitoring only¹ | ❌ | +| Hardware acceleration detection | ✅ surfaced (`hardware_acceleration_enabled`) | ⚠️ core-internal, not surfaced² | ❌² | N/A | | Counter-based nonces | ✅ via Rust | ✅ | ✅ via NAPI (Rust) | ❌ use random | > [!IMPORTANT] > AAD v0x03 is required for protocol compliance. SDKs without it cannot safely interoperate with encrypted payloads from compliant SDKs — the auth tag will fail verification. Python, Rust, and TypeScript construction was code-verified byte-identical on 2026-07-21 (LAB-272); the normative byte layout and the frozen `True`/`False` compressed tokens (protocol#12) are defined in [spec/encryption.md](spec/encryption.md#additional-authenticated-data-aad). Python's auto serializers append the optional `original_type` fifth component; Rust, TypeScript, and interop mode always emit exactly four. Python's read path fails closed when encryption is enabled but a stored entry claims plaintext ([cachekit-py#215](https://github.com/cachekit-io/cachekit-py/pull/215)). +> ¹ **Key rotation ships in no SDK** (LAB-275 audit, corrected from the previous Python ✅ / Rust ✅): cachekit-core's `rotate_key()` is a `NotImplemented` stub (`encryption/core.rs:492`); cachekit-py's `KeyRotationState` PyO3 binding has zero Python callers — only key-fingerprint **mismatch detection** is live (fail-closed, no decrypt-with-previous-key path); cachekit-ts only monitors nonce exhaustion. Rotating a master key today invalidates all encrypted entries. Tracked fleet-wide as LAB-516. +> +> ² Runtime AES detection (`is_x86_feature_detected!("aes")`) lives in cachekit-core and is **surfaced only by Python**. cachekit-rs never re-exports it (the SDK calls the non-metrics encrypt/decrypt entry points); the TS NAPI layer exposes nothing (previous N/A was wrong — the crypto is the same Rust core). Tracked as LAB-523. + --- ## Cache Backends @@ -75,7 +83,7 @@ | Memcached | ✅ `backends/memcached` (`memcached` extra) | ❌ | ❌ | ❌ | | File (local) | ✅ `backends/file` (stdlib + mmap) | ❌ | ❌ | ❌ | | CacheKit SaaS (HTTP) | ✅ `backends/cachekitio` (httpx) | ✅ `cachekitio` feature (reqwest, default) | ✅ `backends/cachekitio.ts` (fetch) | 🔜 Planned | -| Cloudflare Workers | N/A | ✅ `workers` feature (`worker::Fetch`) | ❌ Node 20+ only¹ | N/A | +| Cloudflare Workers | N/A | ✅ `workers` feature (`worker::Fetch`) | ❌ Node 22+ only¹ | N/A | | DynamoDB | ❌² | ❌ | ❌ | ❌ | > ¹ The TS SDK cannot run on the Workers runtime today: crypto is a native NAPI module and the Redis backend is ioredis (TCP). Recorded as ❌ (a plausible target, unsupported) rather than N/A. @@ -120,18 +128,49 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria | Feature | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Circuit breaker | ✅ | ❌ | ✅ | ❌ | -| Backpressure | ✅ | ❌ | ⚠️ Concurrent refresh limits | ❌ | +| Circuit breaker | ✅ wired in decorator path | ❌ (LAB-518) | ✅ preset-gated (on in `production`/`secure`/`io`, off in `minimal`) | ❌ | +| Backpressure | ✅ semaphore + queue admission | ❌ (LAB-518) | ⚠️ SWR refresh cap only (`maxConcurrentRefreshes`)¹ | ❌ | +| Retry (exponential backoff + jitter) | ⚠️ reconnect/lock retry only — no generic backend retry (LAB-522) | ❌ `is_retryable()` classification exists, never called (LAB-518) | ✅ wired (`reliability/retry.ts`, crypto-PRNG jitter) | ❌ | +| Graceful degradation (fail-open to wrapped fn) | ✅ (encrypted paths stay fail-closed) | ❌ first error propagates (LAB-518) | ✅ on by default (`withDegradation`) | ❌ | | Distributed locking | ✅ Redis + SaaS backends | ✅ SaaS backend only (`LockableBackend`; Redis/Workers lack impls) | ✅ SaaS backend only | ❌ | | L1/L2 dual-layer cache | ✅ | ✅ moka (native) / `l1` feature | ✅ | ❌ | -| Cache stampede prevention | ✅ | ❌ | ✅ Version tokens + background L1 refresh | ❌ | +| Cache stampede prevention | ✅ async decorators (backend lock); sync path none by design² | ❌ lock primitive never wired (LAB-518) | ⚠️ refresh path only — no cold-miss single-flight (LAB-519)³ | ❌ | +| Cross-instance invalidation (pub/sub) | ❌ built but never wired (LAB-520)⁴ | ❌ | ✅ Redis pub/sub channel, wired | ❌ | | TTL management | ✅ Redis + SaaS | ✅ Redis + SaaS (`TtlInspectable`; Workers ❌) | ✅ SaaS only (`TTLBackend`) | ❌ | -| Stale-while-revalidate (server stale-grace) | 🚧 LAB-381 | ❌ | ❌ | ❌ | +| Stale-while-revalidate (client, L1) | ⚠️ L1-only mode (`ObjectCache`); backed modes dead code (LAB-388)⁵ | ❌ | ✅ `getWithSwr` + version tokens + background refresh | ❌ | +| Stale-while-revalidate (server stale-grace) | ❌ (spec'd, 🚧 LAB-381) | ❌ | ❌ | ❌ | + +> ¹ Deliberate-partial: Node's event-loop model concentrates herd risk in refresh storms (capped) and cold misses (LAB-519); a Python-style admission semaphore is not idiomatic. Recorded as a decision, not a gap. +> +> ² cachekit-py's sync decorator documents "use async decorators" for distributed locking (`decorators/wrapper.py:1043`) — a design decision, not drift. +> +> ³ TS dedups SWR *refreshes* (`refreshingKeys` + version tokens) but on a cold L1+L2 miss concurrent callers all execute the wrapped function (`cache.ts:392`); the `LockableBackend` capability exists unwired. Under metered-misses pricing a cold-key herd multiplies billed misses. +> +> ⁴ cachekit-py ships a complete `invalidation/` pub/sub package (Redis channel, event levels) with zero references outside itself + tests — multi-pod L1 invalidation does not actually work in Python. TS ships the equivalent live (`invalidation/redis-channel.ts`). +> +> ⁵ Python's live client SWR is `ObjectCache.get_with_swr` (L1-only mode, wired in the decorator); the byte-layer `L1Cache.get_with_swr` used by backed modes is dead code — LAB-388. > **Lock id transport (CWE-532):** the unlock call carries the lock capability token in the `X-CacheKit-Lock-Id` request header, never the `?lock_id=` query string (which leaks via access/proxy logs and OTel `http.url` spans). **Migration complete in all three SDKs** (verified 2026-07-20, LAB-273): Python (#131, closed), Rust (#24, closed), TypeScript ships the header (ts#63 remains open only for an unrelated NAPI-rebuild item). SaaS dual-reads both during the rollout window. See [spec/saas-api.md](spec/saas-api.md#delete-v1cachekeylock). --- +## Observability + +Audited against code 2026-07-21 (LAB-275). "Wired" means the live cache path feeds it with zero user plumbing. + +| Feature | Python | Rust | TypeScript | PHP | +| :--- | :---: | :---: | :---: | :---: | +| Metrics | ✅ live `prometheus_client` counters/gauges/histograms (default registry; no HTTP exposition helper — py#146) | ⚠️ `L1Stats` SaaS header telemetry only, requires a user-supplied `MetricsProvider` nothing auto-wires (LAB-521) | ❌ Prometheus module exists but is dead — `metrics` option accepted and **silently ignored** (LAB-517)¹ | ❌ | +| Structured logging | ✅ JSON ring-buffer logger with sensitive-data masking | ❌ no `log`/`tracing` integration (LAB-521) | ❌ ad-hoc `console.error` only (LAB-517) | ❌ | +| Distributed tracing (OTel) | ❌ span-shaped API is a no-op (`NoOpSpan`) | ❌ | ❌ | ❌ | +| SaaS telemetry headers (`X-CacheKit-L1-*`) | ✅ auto | ⚠️ reports `disabled` unless user wires a provider (LAB-521) | ⚠️ user-supplied `metricsProvider`, never auto-wired | ❌ | + +> ¹ The silent no-op is the trust bug: every intent preset accepts `metrics` (`intents.ts`), `CacheImpl` never reads it, and the Prometheus module isn't exported. LAB-517 wires it or rejects the option. +> +> Distributed tracing is absent in **every** SDK (a fleet-wide roadmap item, not a parity gap); Python's no-op span API should not be mistaken for tracing support. + +--- + ## Developer Experience | Feature | Python | Rust | TypeScript | PHP | @@ -141,7 +180,7 @@ The contract a storage backend must satisfy per SDK (bytes in / bytes out; seria | Builder API | ✅ | ✅ `CacheKit::builder()` / `from_env()` | ✅ | ❌ | | Async support | ✅ | ✅ | ✅ | ❌ | | Sync support | ✅ | ✅ | ❌ | ✅ | -| WASM / CF Workers | N/A | ✅ `workers` feature (`?Send`, `Rc`) | N/A | N/A | +| WASM / CF Workers | N/A | ✅ `workers` feature (`?Send`, `Rc`) | ❌ blocked by NAPI crypto + ioredis (LAB-431) | N/A | | pydantic-settings config | ✅ | N/A | N/A | N/A | | Type hints / strict types | ✅ | ✅ | ✅ | ✅ PHP 8.1+ | @@ -166,23 +205,23 @@ its spec: | Requirement | Python | Rust | TypeScript | PHP | | :--- | :---: | :---: | :---: | :---: | -| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (no spec'd keygen)¹ — interop keygen 🚧 [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ Compliant | ⚠️ Untested | +| Key generation (Blake2b) | ✅ Compliant | N/A auto mode (no spec'd keygen)¹ — interop keygen ✅ merged ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Compliant | ⚠️ Untested | | Wire format (ByteStorage) | ✅ Compliant² | ✅ Canonical (`cachekit-core`) — unused for stored values² | ✅ Compliant | ⚠️ Untested | | Storage container (auto mode)² | CK v3 frame (Python-internal) | Plain MessagePack (`rmp` named) — no envelope | Bare ByteStorage envelope (default) | — | | 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) | ✅ Compliant (4 components) | ✅ Compliant (4 components) | ❌ Not implemented | | SaaS API | ✅ Compliant | ✅ Compliant (CachekitIO backend) | ✅ Compliant | ❌ Not implemented | -| Test vectors in CI³ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) | ❌ None on `main` — interop vectors land with [#33](https://github.com/cachekit-io/cachekit-rs/pull/33) | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors | ⚠️ Pending | -| Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Merged ([#220](https://github.com/cachekit-io/cachekit-py/pull/220), unreleased) | 🚧 In review ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33)) | ✅ Merged ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71), unreleased) | ❌ Not implemented | +| Test vectors in CI³ | ✅ interop/v1 (full set, incl. AAD + encryption through the real stack) | ✅ interop/v1 vectors vendored + run by `cargo test` in CI ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33), merged 2026-07-21) | ✅ interop/v1 (full set, incl. its key vectors) + inline Python-generated AAD-construction and encryption (decrypt-Python-ciphertext) vectors | ⚠️ Pending | +| Interop mode ([spec](spec/interop-mode.md), opt-in) | ✅ Released (v0.13.0; [#220](https://github.com/cachekit-io/cachekit-py/pull/220)) | ✅ Merged ([#33](https://github.com/cachekit-io/cachekit-rs/pull/33), unreleased — crates.io still 0.3.0) | ✅ Merged ([#71](https://github.com/cachekit-io/cachekit-ts/pull/71), unreleased — npm still 0.1.2) | ❌ Not implemented | > [!NOTE] -> ¹ "N/A" for Rust key generation means `cachekit-rs` implements no spec'd key format: `get`/`set` take caller-supplied keys, and the `#[cachekit]` macro (which has **no** `key` parameter) derives an SDK-internal legacy key — `{namespace}:{blake2b256-hex}` over the *unqualified* function name, the obsolete RFC §3.1.5 shape matching **no** current protocol key format, never usable cross-SDK. That derivation is macro-internal plumbing (it IS called at runtime by every `#[cachekit]` expansion — the LAB-424 "unused" premise was a grep miss), removed from the public API in [cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35). `cachekit-core` is a protocol primitive library with no keygen. Spec-conformant Rust keygen arrives with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33)). +> ¹ "N/A" for Rust key generation means `cachekit-rs` implements no spec'd key format: `get`/`set` take caller-supplied keys, and the `#[cachekit]` macro (which has **no** `key` parameter) derives an SDK-internal legacy key — `{namespace}:{blake2b256-hex}` over the *unqualified* function name, the obsolete RFC §3.1.5 shape matching **no** current protocol key format, never usable cross-SDK. That derivation is macro-internal plumbing (it IS called at runtime by every `#[cachekit]` expansion — the LAB-424 "unused" premise was a grep miss), removed from the public API in [cachekit-rs#35](https://github.com/cachekit-io/cachekit-rs/pull/35). `cachekit-core` is a protocol primitive library with no keygen. Spec-conformant Rust keygen arrived with interop mode ([cachekit-rs#33](https://github.com/cachekit-io/cachekit-rs/pull/33), merged 2026-07-21). > [!NOTE] > ² 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). > [!NOTE] -> ³ "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`, and `python-frame.json` against reference implementations. **Not verified by any CI anywhere**: `wire-format.json` (tracked as LAB-423) and `cache-keys.json` (generated by cachekit-py v0.5.0; tracked as LAB-425). +> ³ "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`, and `python-frame.json` against reference implementations. The former coverage holes are closed as of 2026-07-21: `wire-format.json` is byte-verified in cachekit-core CI ([core#55](https://github.com/cachekit-io/cachekit-core/pull/55), LAB-423) and `cache-keys.json` is vendored sha256-pinned into cachekit-py and byte-verified on every default CI run (LAB-425, protocol#26). **Every vector file is now enforced by at least one CI.** --- @@ -193,7 +232,8 @@ its spec: - Hybrid Python-Rust architecture: decorators and orchestration in Python, ByteStorage and encryption in Rust (via PyO3) - The `cachekit-core` Rust crate is the canonical implementation for compression, checksums, and encryption -- 4 serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON), Arrow (columnar) +- 4 registry serializers: Standard (cross-language), Auto (Python-optimized), Orjson (JSON, `[json]` extra), Arrow (columnar, `[data]` extra) — plus the non-registry interop/v1 serializer for cross-SDK mode +- Live Prometheus metrics (`prometheus_client`, default registry) + structured JSON logging with sensitive-data masking - Backends: Redis, Memcached, File (local), CacheKit SaaS — backend auto-detected from the environment: a single unambiguous selector, with `REDIS_URL`/localhost fallback, and ambiguous (multiple) selectors raising `ConfigurationError` (see [Cache Backends](#cache-backends)) - Config via pydantic-settings; secrets via `SecretStr` @@ -203,21 +243,21 @@ its spec: Rust SDK (cachekit-rs) - Published on crates.io as `cachekit-rs` v0.3.0 + `cachekit-macros` v0.3.0 -- Feature flags: `redis`, `cachekitio`, `encryption`, `l1`, `macros`, `workers` +- Feature flags: `redis`, `cachekitio`, `encryption`, `l1`, `macros`, `workers`, `unsync` (default = `cachekitio` + `encryption` + `l1`); MSRV 1.85 - Backends: `RedisBackend` (fred), `CachekitIO` (reqwest), `WorkersCachekitIO` (CF Workers fetch) - L1 cache via moka (native only, `l1` feature) - `#[cachekit]` proc-macro for decorator-style caching - `SecureCache` for zero-knowledge encrypted caching - SSRF protection, credential redaction, `Zeroizing` key material - WASM/Workers support: `?Send` + `Rc` paths via `cfg(target_arch = "wasm32")` -- Depends on `cachekit-core` v0.2.0 for ByteStorage and encryption primitives +- Depends on `cachekit-core` v0.3 for ByteStorage and encryption primitives
Rust Core (cachekit-core) -- Published on crates.io as `cachekit-core` v0.3.0 (`cachekit-rs` still depends on the 0.2 line — Renovate bump tracked separately) +- Published on crates.io as `cachekit-core` v0.3.0 (`cachekit-rs` depends on the 0.3 line since [cachekit-rs#28](https://github.com/cachekit-io/cachekit-rs/pull/28)) - Provides: `ByteStorage`, `ZeroKnowledgeEncryptor`, `derive_domain_key`, `derive_tenant_keys` - Dependencies: `lz4_flex`, `xxhash-rust`, `ring` (native) / `aes-gcm` (wasm32), `hkdf`, `sha2`, `rmp-serde` - Formally verified security properties via Kani @@ -236,8 +276,7 @@ its spec: - Circuit breaker (rolling window), retry (exponential backoff + jitter), graceful degradation - Distributed locking via CacheKit SaaS backend - Intent-based API: `createCache.minimal()`, `.production()`, `.secure()`, `.io()` -- 457 tests, 93.75% statement coverage -- Dual output: ESM + CJS, Node 20+ +- Dual output: ESM + CJS, Node 22+ (async-only API)