From 28fd68daf6ef247c74e9099e3084a1b1801de0e6 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Tue, 14 Jul 2026 22:56:37 +1000 Subject: [PATCH 1/2] test: stop docs conftest leaking CACHEKIT_MASTER_KEY into the env (#205) docs/conftest.py set CACHEKIT_MASTER_KEY process-wide so @cache.secure fences would validate. Since PR #127 (auto-detect) and PR #200 (settings re-read), an ambient master key turns encryption on globally, and the v0.6.0 cross-SDK rule then rejects every plain @cache fence using a non-cross-SDK serializer at decoration time. That failed 5 fences and kept the post-merge markdown-docs job red on main. Fix mirrors the tests/conftest.py fix for the same trap: expose secret_key as a global for fences to pass explicitly (master_key=secret_key) and do not set the env var. All executed @cache.secure fences already pass the key explicitly, so none regress (126 fences green). The markdown-docs suite only runs post-merge (push to main), not on PRs, so a regression would re-redden main instead of failing review. Added a fast unit guard (runs on PRs) that fails if the hook sets the env var again. --- docs/CONTRIBUTING.md | 7 ++- docs/conftest.py | 13 ++++-- tests/unit/test_docs_conftest_no_key_leak.py | 48 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 tests/unit/test_docs_conftest_no_key_leak.py diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 5bf4bed..a988fad 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -60,8 +60,11 @@ These functions are provided for documentation examples and return mock data: - `process_tenant_request(tenant_id, request)` - Returns tenant result #### Configuration -- `secret_key` - Test encryption key (value: `"a" * 64`) -- `CACHEKIT_MASTER_KEY` - Environment variable set to `secret_key` (enables `@cache.secure` examples) +- `secret_key` - Test encryption key (value: `"a" * 64`). `@cache.secure` examples must + pass it explicitly (`master_key=secret_key`); the conftest does **not** set + `CACHEKIT_MASTER_KEY` in the environment, because an ambient key turns encryption on + globally and breaks plain `@cache` fences that use `serializer="auto"` or custom + serializers. ## Skipping Examples with `notest` diff --git a/docs/conftest.py b/docs/conftest.py index e47447e..4318807 100644 --- a/docs/conftest.py +++ b/docs/conftest.py @@ -6,7 +6,6 @@ """ import logging -import os import time try: @@ -91,10 +90,16 @@ def expensive_computation(): logger = logging.getLogger("cachekit.examples") logger.setLevel(logging.INFO) - # Secret key for encryption examples (test value only) + # Secret key for encryption examples (test value only). Exposed as a global + # so @cache.secure fences can pass it explicitly (master_key=secret_key). + # + # We deliberately DO NOT set CACHEKIT_MASTER_KEY in the environment here. + # Since the PR #127 auto-detect + PR #200 settings re-read, an ambient + # master key turns encryption on globally, and the v0.6.0 cross-SDK rule + # then rejects any plain `@cache` fence using a non-cross-SDK serializer + # (serializer="auto", custom serializer instances) at decoration time. + # tests/conftest.py hit the same trap and fixed it the same way. See #205. secret_key = "a" * 64 # 32 bytes in hex - # Set env var so @cache.secure validation passes - os.environ["CACHEKIT_MASTER_KEY"] = secret_key globals_dict = { "cache": cache, diff --git a/tests/unit/test_docs_conftest_no_key_leak.py b/tests/unit/test_docs_conftest_no_key_leak.py new file mode 100644 index 0000000..f514333 --- /dev/null +++ b/tests/unit/test_docs_conftest_no_key_leak.py @@ -0,0 +1,48 @@ +"""Regression guard for issue #205. + +The markdown-docs suite runs only in the post-merge CI job (push to main), not +on PRs, so a regression here would turn `main` red instead of failing review. +This fast unit test runs on every PR and fails loudly if `docs/conftest.py` +starts setting CACHEKIT_MASTER_KEY in the process environment again. + +Why it matters: an ambient master key turns encryption on globally (PR #127 +auto-detect + PR #200 settings re-read), and the v0.6.0 cross-SDK rule then +rejects every plain `@cache` doc fence that uses a non-cross-SDK serializer +(serializer="auto", custom serializer instances) at decoration time. +""" + +from __future__ import annotations + +import importlib.util +import os +from pathlib import Path + +import pytest + +DOCS_CONFTEST = Path(__file__).resolve().parents[2] / "docs" / "conftest.py" + + +def _load_docs_conftest(): + spec = importlib.util.spec_from_file_location("docs_conftest_under_test", DOCS_CONFTEST) + assert spec and spec.loader, f"could not load {DOCS_CONFTEST}" + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.mark.unit +def test_docs_globals_hook_does_not_set_master_key_env(monkeypatch: pytest.MonkeyPatch) -> None: + """Invoking the markdown-docs globals hook must not leak CACHEKIT_MASTER_KEY.""" + monkeypatch.delenv("CACHEKIT_MASTER_KEY", raising=False) + + module = _load_docs_conftest() + globals_dict = module.pytest_markdown_docs_globals() + + assert "CACHEKIT_MASTER_KEY" not in os.environ, ( + "docs/conftest.py set CACHEKIT_MASTER_KEY in the environment. This turns " + "encryption on globally and breaks plain @cache fences using serializer='auto' " + "or custom serializers. Pass the key explicitly (master_key=secret_key) in the " + "@cache.secure fences instead. See issue #205." + ) + # The key must still be available to fences that opt in explicitly. + assert globals_dict.get("secret_key") == "a" * 64 From d7bdd6d57404177720a0aae4c96d2c1e339fc5e6 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 19 Jul 2026 02:07:40 +1000 Subject: [PATCH 2/2] docs: remove comma before restrictive 'because' clause in CONTRIBUTING Co-authored-by: multica-agent --- docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a988fad..145c238 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -62,7 +62,7 @@ These functions are provided for documentation examples and return mock data: #### Configuration - `secret_key` - Test encryption key (value: `"a" * 64`). `@cache.secure` examples must pass it explicitly (`master_key=secret_key`); the conftest does **not** set - `CACHEKIT_MASTER_KEY` in the environment, because an ambient key turns encryption on + `CACHEKIT_MASTER_KEY` in the environment because an ambient key turns encryption on globally and breaks plain `@cache` fences that use `serializer="auto"` or custom serializers.