Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion spec/cache-key-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -32,6 +33,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}
```
Expand Down Expand Up @@ -78,6 +86,27 @@ 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 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. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Violates team rule 'Avoid the 'any' type': Detect the use of the 'any' type in TypeScript. Using 'any' disables type checking and can lead to runtime errors. Recommend using specific types or generics instead.

Prompt for LLM

File spec/cache-key-format.md:

Line 102:

Violates team rule 'Avoid the 'any' type': Detect the use of the 'any' type in TypeScript. Using 'any' disables type checking and can lead to runtime errors. Recommend using specific types or generics instead.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


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:
Expand Down Expand Up @@ -191,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.

Expand Down
19 changes: 12 additions & 7 deletions spec/interop-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
> [npm](https://www.npmjs.com/package/@cachekit-io/cachekit) 0.1.3+, Rust on
> [crates.io](https://crates.io/crates/cachekit-rs) 0.4.0+ — floors, not snapshots; consult
> each registry or the [SDK feature matrix](../sdk-feature-matrix.md#compliance-status) for current versions.
> Server-side: 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)).
> Design discussion: [Issue #1](https://github.com/cachekit-io/protocol/issues/1) ·
> Test vectors: [`test-vectors/interop-mode.json`](../test-vectors/interop-mode.json) ·
> Reference implementation: [`tools/interop-reference.py`](../tools/interop-reference.py)
Expand Down Expand Up @@ -375,13 +378,15 @@ bytes ([saas-api.md](saas-api.md)). Interop keys carry **no `ns:` prefix**; the
`{namespace}` segment is an SDK-level convention, not a SaaS routing element (tenant
isolation comes from authentication, not key parsing).

> [!WARNING]
> The deployed SaaS cache-key validator currently enforces auto-mode grammar and
> would reject interop-format keys. Shrinking that validator to security-only checks
> is tracked in [saas#91](https://github.com/cachekit-io/saas/issues/91) and MUST land
> before interop mode ships against the CachekitIO backend. The interop segment
> grammar (lowercase, no `:` beyond the two delimiters, no `/`, max 194 chars) is
> deliberately a strict subset of what a security-only validator accepts.
> [!NOTE]
> The SaaS cache-key validator was shrunk to security-only checks
> ([saas#91](https://github.com/cachekit-io/saas/issues/91), landed in
> [saas#231](https://github.com/cachekit-io/saas/pull/231)) — the deployed validator
> accepts interop-format keys; see
> [cache-key-format.md → Server-Side Requirements](cache-key-format.md#server-side-requirements).
> The interop segment grammar (lowercase, no `:` beyond the two delimiters, no `/`,
> max 194 chars) is deliberately a strict subset of what the security-only
> validator accepts.

---

Expand Down
4 changes: 2 additions & 2 deletions test-vectors/cache-keys.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"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 \u2014 only the args hash portion must match.",
"key_format": "ns:{namespace}:func:{module}.{qualname}:args:{blake2b_256_hex}:{ic_flag}{serializer_code}",
"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": [
{
Expand Down
Loading