From f817582fd561fb07c424b7df0ac56c2a1ad48094 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Fri, 17 Jul 2026 19:41:35 +1000 Subject: [PATCH 1/2] docs(spec): reframe 7-segment key format as SDK convention; add server-side requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CachekitIO validator is now security-only (saas#91): percent-decode, length cap, charset whitelist, '..' rejection, ns:/nsapi: namespace shape. The 7-segment grammar is Python SDK convention, not a server contract — TS/Rust {ns}:{hash} and interop-mode keys are valid on the wire. - cache-key-format.md: new Server-Side Requirements section + note on the Full Key Structure - interop-mode.md: status DRAFT -> SERVER-SUPPORTED (SDK generation still pending, Issue #1) - test-vectors/cache-keys.json: key_format/note fields updated Co-authored-by: multica-agent --- spec/cache-key-format.md | 28 ++++++++++++++++++++++++++++ spec/interop-mode.md | 6 ++++-- test-vectors/cache-keys.json | 4 ++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/spec/cache-key-format.md b/spec/cache-key-format.md index 483fdfd..364bf54 100644 --- a/spec/cache-key-format.md +++ b/spec/cache-key-format.md @@ -15,6 +15,7 @@ ## Table of Contents - [Key Format](#key-format) +- [Server-Side Requirements](#server-side-requirements) - [Cross-SDK Key Generation Strategy](#cross-sdk-key-generation-strategy) - [Argument Hashing Algorithm](#argument-hashing-algorithm) - [Character Normalization](#character-normalization) @@ -31,6 +32,13 @@ ### Full Key Structure +> [!NOTE] +> This 7-segment structure is the **Python SDK's internal convention**, not a +> server requirement. The CachekitIO backend validates keys security-only (see +> [Server-Side Requirements](#server-side-requirements)) and otherwise treats +> them as opaque strings — TypeScript/Rust `{ns}:{hash}` keys and +> [Interop Mode](interop-mode.md) keys are equally valid on the wire. + ``` ns:{namespace}:func:{module}.{qualname}:args:{blake2b_hash}:{ic_flag}{serializer_code} ``` @@ -77,6 +85,26 @@ ns:cache:func:app.views.index:args:0000...0000:0s --- +## Server-Side Requirements + +The CachekitIO SaaS stores keys as opaque strings; the ONLY structure it +enforces is security-relevant (per `saas` issue #91 / SRP refactor): + +| Check | Rule | +| :--- | :--- | +| Transport | Key is percent-encoded into the URL path; the server decodes it once. | +| Length | Decoded key ≤ 400 characters. | +| Charset | `[a-zA-Z0-9_.:-]` only — no `/` (sub-resource routing), no `%`, no control chars. | +| Traversal | `..` is rejected anywhere in the key. | +| Namespace | Keys starting `ns:{namespace}:` or `nsapi:{namespace}:` must have a namespace of 1–64 chars of `[a-zA-Z0-9_-]`. Keys without either prefix scope to the `default` namespace. | +| Write spaces | `ns:` keys are mutable only by SDK (`ck_sdk_`) API keys; `nsapi:` keys only by direct (`ck_api_`) API keys. Reads are open to both. Legacy `ck_live_` keys are exempt. | + +Everything else in this document — segment count, `func:`/`args:` literals, +hash length, metadata flags — is SDK convention for deterministic key +generation, invisible to the server. + +--- + ## Cross-SDK Key Generation Strategy For multi-language interoperability, all SDKs MUST use **explicit namespaces** rather than auto-generated function signatures. The `func:` segment is inherently language-specific (Python modules vs PHP namespaces vs Go packages), so cross-language cache sharing requires: diff --git a/spec/interop-mode.md b/spec/interop-mode.md index fe8c2c3..d4f79bd 100644 --- a/spec/interop-mode.md +++ b/spec/interop-mode.md @@ -6,8 +6,10 @@ **Language-neutral key format for sharing cache entries across SDK implementations.** -> **Status**: DRAFT — See [Issue #1](https://github.com/cachekit-io/protocol/issues/1) for discussion. -> This mode is NOT yet implemented in any SDK. +> **Status**: SERVER-SUPPORTED — the CachekitIO validator accepts interop-format +> keys (`{namespace}:{operation}:{args_hash}` scopes to the `default` namespace; +> see [cache-key-format.md → Server-Side Requirements](cache-key-format.md#server-side-requirements)). +> SDK-side generation is not yet implemented — see [Issue #1](https://github.com/cachekit-io/protocol/issues/1). diff --git a/test-vectors/cache-keys.json b/test-vectors/cache-keys.json index b63fbe6..6681de8 100644 --- a/test-vectors/cache-keys.json +++ b/test-vectors/cache-keys.json @@ -1,8 +1,8 @@ { "version": "1.0.0", "generator": "cachekit-py v0.5.0", - "note": "Keys include __main__ module because vectors were generated at top level. Cross-SDK implementations should substitute their own module path \u2014 only the args hash portion must match.", - "key_format": "ns:{namespace}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{serializer_code}", + "note": "Keys include __main__ module because vectors were generated at top level. Cross-SDK implementations should substitute their own module path — only the args hash portion must match. The 7-segment shape is SDK-internal convention: the CachekitIO server accepts any key passing its security-only checks (length, charset, namespace prefix shape), including TS/Rust {ns}:{hash} and interop-mode keys.", + "key_format": "ns:{namespace}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{serializer_code} (Python SDK convention — server validates security-only, see spec/cache-key-format.md#server-side-requirements)", "hash_algorithm": "blake2b-256 of msgpack([normalized_args, sorted_kwargs])", "vectors": [ { From ff89ed1f9be4cfcda7841b0a6ff19092493d8222 Mon Sep 17 00:00:00 2001 From: Winston Date: Wed, 2 Sep 2026 09:38:49 +1000 Subject: [PATCH 2/2] docs(spec): document default-namespace write space; mark cache-keys.json Python-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the two CodeRabbit findings on #17. Server-Side Requirements omitted the rule the deployed validator actually has for unprefixed keys: saas apps/cache/src/cache-key-validator.ts classes them `open` (namespace `default`) and the write-space 403 gate in index.ts only fires for the `sdk`/`api` classes. So interop-mode and TS/Rust {ns}:{hash} keys are writable by any key class and get none of the intra-tenant write-space isolation. That is a consequence SDK implementors need stated, not inferred. Also states that per-key namespace grants still apply (a namespace-restricted key must include `default`) and that ck_live_ has no server-side retirement date — recorded as fact, not policy: this spec describes the deployed validator, it does not legislate for it. cache-keys.json's note told other SDKs to "substitute their own module path; only the args hash must match". A different func: segment is a different key, so that can never yield a shared-cache hit, and the arg-hash algorithm itself (msgpack([args, sorted_kwargs]) with Python normalization) is not what interop/v1 hashes. The vectors are Python-SDK-only; cross-SDK conformance is interop-mode.json. The same claim in the spec's Test Vectors section is fixed to match. Vector bytes untouched; cachekit-py vendors its own sha256-pinned copy so its CI is unaffected. --- spec/cache-key-format.md | 5 +++-- test-vectors/cache-keys.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/cache-key-format.md b/spec/cache-key-format.md index 19d5e90..78f2b4c 100644 --- a/spec/cache-key-format.md +++ b/spec/cache-key-format.md @@ -98,7 +98,8 @@ enforces is security-relevant (per `saas` issue #91 / SRP refactor): | Charset | `[a-zA-Z0-9_.:-]` only — no `/` (sub-resource routing), no `%`, no control chars. | | Traversal | `..` is rejected anywhere in the key. | | Namespace | Keys starting `ns:{namespace}:` or `nsapi:{namespace}:` must have a namespace of 1–64 chars of `[a-zA-Z0-9_-]`. Keys without either prefix scope to the `default` namespace. | -| Write spaces | `ns:` keys are mutable only by SDK (`ck_sdk_`) API keys; `nsapi:` keys only by direct (`ck_api_`) API keys. Reads are open to both. Legacy `ck_live_` keys are exempt. | +| Write spaces | `ns:` keys are mutable only by SDK (`ck_sdk_`) API keys; `nsapi:` keys only by direct (`ck_api_`) API keys. Reads are open to both. Legacy `ck_live_` keys predate the split and are exempt from it — they may write either class. No server-side retirement date is set for `ck_live_`. | +| Default namespace | Keys with neither prefix (TypeScript/Rust `{ns}:{hash}`, [Interop Mode](interop-mode.md) keys, bare hashes) are an **open** write space: any key class may write them, so the intra-tenant write-space isolation above does not protect them. Per-key namespace grants still apply — an API key restricted to named namespaces must include `default` to read or write unprefixed keys. | Everything else in this document — segment count, `func:`/`args:` literals, hash length, metadata flags — is SDK convention for deterministic key @@ -219,7 +220,7 @@ After key construction, the following characters are replaced: ## Test Vectors -[`test-vectors/cache-keys.json`](../test-vectors/cache-keys.json) contains 10 auto-mode key vectors (`args` + `kwargs` + metadata → `expected_key`) covering primitives, mixed args/kwargs, `null`, booleans, nested dicts, and the no-namespace form. Keys were generated at top level, so the `func:` segment is `__main__.{qualname}` — cross-SDK implementations substitute their own module path; only the args-hash segment must match byte-for-byte. +[`test-vectors/cache-keys.json`](../test-vectors/cache-keys.json) contains 10 auto-mode key vectors (`args` + `kwargs` + metadata → `expected_key`) covering primitives, mixed args/kwargs, `null`, booleans, nested dicts, and the no-namespace form. Keys were generated at top level, so the `func:` segment is `__main__.{qualname}`. These vectors are **Python-SDK-only**: the `func:` segment is language-specific, so no other SDK can reproduce these keys or share the cache entries they name. Cross-SDK conformance uses [`test-vectors/interop-mode.json`](../test-vectors/interop-mode.json) (see [Interop Mode](interop-mode.md)). Enforcement: the vectors are vendored (sha256-pinned) into cachekit-py and byte-verified against `CacheKeyGenerator` on every default CI run (`tests/unit/protocol/test_cache_key_vectors.py`). A vector failing there is a key-stability break to triage — never silently regenerate: a changed key orphans every existing cache entry and turns the fleet's hits into billed misses. diff --git a/test-vectors/cache-keys.json b/test-vectors/cache-keys.json index 003d242..35ecd09 100644 --- a/test-vectors/cache-keys.json +++ b/test-vectors/cache-keys.json @@ -2,7 +2,7 @@ "version": "1.0.0", "generator": "cachekit-py v0.12.0", "ci_verification": "Vendored (sha256-pinned) and byte-verified against CacheKeyGenerator in cachekit-py default CI: tests/unit/protocol/test_cache_key_vectors.py", - "note": "Keys include __main__ module because vectors were generated at top level. Cross-SDK implementations should substitute their own module path — only the args hash portion must match. The 7-segment shape is SDK-internal convention: the CachekitIO server accepts any key passing its security-only checks (length, charset, namespace prefix shape), including TS/Rust {ns}:{hash} and interop-mode keys.", + "note": "Python-SDK-only conformance vectors. The 7-segment auto-mode key embeds a language-specific func: segment (__main__ here because the vectors were generated at top level), so no other SDK can reproduce these keys or share the cache entries they name; cross-SDK validation uses test-vectors/interop-mode.json (Interop Mode). The 7-segment shape is SDK-internal convention: the CachekitIO server accepts any key passing its security-only checks (length, charset, namespace prefix shape), including TS/Rust {ns}:{hash} and interop-mode keys.", "key_format": "ns:{namespace}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{serializer_code} (Python SDK convention — server validates security-only, see spec/cache-key-format.md#server-side-requirements)", "hash_algorithm": "blake2b-256 of msgpack([normalized_args, sorted_kwargs])", "vectors": [