From 1c2707d45ed6e4263eb2f81cfe966af1a6574326 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:32:47 -0500 Subject: [PATCH 01/11] feat: build bounded Sky Cache core --- cache.py | 192 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 172 insertions(+), 20 deletions(-) diff --git a/cache.py b/cache.py index 4695443..b0f1920 100644 --- a/cache.py +++ b/cache.py @@ -2,54 +2,206 @@ import threading import time +from collections import OrderedDict +from collections.abc import Callable from dataclasses import dataclass from typing import Any +MAX_KEY_LENGTH = 512 +MAX_TTL_SECONDS = 31_536_000 + + +class VersionConflict(RuntimeError): + """Raised when an optimistic cache update targets a stale version.""" + @dataclass(frozen=True) class CacheEntry: value: Any expires_at: float | None + version: int -class DistributedCache: - """Thread-safe TTL cache abstraction with explicit invalidation semantics.""" +class SkyCache: + """Thread-safe bounded single-process TTL/LRU cache with optimistic writes.""" - def __init__(self, default_ttl_seconds: float | None = None) -> None: - if default_ttl_seconds is not None and default_ttl_seconds < 0: - raise ValueError("default_ttl_seconds must be non-negative") + def __init__( + self, + default_ttl_seconds: float | None = None, + max_entries: int = 10_000, + *, + clock: Callable[[], float] = time.monotonic, + ) -> None: + self._validate_ttl(default_ttl_seconds) + if not 1 <= max_entries <= 1_000_000: + raise ValueError("max_entries must be between 1 and 1,000,000") self._default_ttl = default_ttl_seconds - self._entries: dict[str, CacheEntry] = {} + self._max_entries = max_entries + self._clock = clock + self._entries: OrderedDict[str, CacheEntry] = OrderedDict() self._lock = threading.RLock() + self._version = 0 + self._stats = { + "hits": 0, + "misses": 0, + "sets": 0, + "deletes": 0, + "evictions": 0, + "expirations": 0, + "cas_conflicts": 0, + } - def set(self, key: str, value: Any, ttl_seconds: float | None = None) -> None: - if not key.strip(): + @staticmethod + def _validate_key(key: str) -> str: + if not isinstance(key, str): + raise TypeError("cache key must be a string") + if not key or key.isspace(): raise ValueError("cache key is required") + if len(key) > MAX_KEY_LENGTH: + raise ValueError(f"cache key cannot exceed {MAX_KEY_LENGTH} characters") + return key + + @staticmethod + def _validate_ttl(ttl_seconds: float | None) -> None: + if ttl_seconds is None: + return + if not isinstance(ttl_seconds, (int, float)): + raise TypeError("ttl_seconds must be numeric") + if ttl_seconds < 0 or ttl_seconds > MAX_TTL_SECONDS: + raise ValueError( + f"ttl_seconds must be between 0 and {MAX_TTL_SECONDS} seconds" + ) + + def _next_version(self) -> int: + self._version += 1 + return self._version + + def _expires_at(self, ttl_seconds: float | None) -> float | None: ttl = self._default_ttl if ttl_seconds is None else ttl_seconds - if ttl is not None and ttl < 0: - raise ValueError("ttl_seconds must be non-negative") - expires_at = None if ttl is None else time.monotonic() + ttl + self._validate_ttl(ttl) + return None if ttl is None else self._clock() + ttl + + def _remove_if_expired(self, key: str, now: float) -> bool: + entry = self._entries.get(key) + if entry is None or entry.expires_at is None or now < entry.expires_at: + return False + del self._entries[key] + self._stats["expirations"] += 1 + return True + + def _evict_if_needed(self) -> None: + while len(self._entries) > self._max_entries: + self._entries.popitem(last=False) + self._stats["evictions"] += 1 + + def set(self, key: str, value: Any, ttl_seconds: float | None = None) -> int: + key = self._validate_key(key) + expires_at = self._expires_at(ttl_seconds) with self._lock: - self._entries[key] = CacheEntry(value=value, expires_at=expires_at) + version = self._next_version() + self._entries[key] = CacheEntry(value=value, expires_at=expires_at, version=version) + self._entries.move_to_end(key) + self._stats["sets"] += 1 + self._evict_if_needed() + return version - def get(self, key: str, default: Any = None) -> Any: + def set_if_absent( + self, + key: str, + value: Any, + ttl_seconds: float | None = None, + ) -> tuple[bool, int]: + key = self._validate_key(key) + with self._lock: + self._remove_if_expired(key, self._clock()) + existing = self._entries.get(key) + if existing is not None: + return False, existing.version + return True, self.set(key, value, ttl_seconds) + + def compare_and_set( + self, + key: str, + value: Any, + expected_version: int, + ttl_seconds: float | None = None, + ) -> int: + key = self._validate_key(key) + if expected_version < 1: + raise ValueError("expected_version must be positive") with self._lock: + self._remove_if_expired(key, self._clock()) + existing = self._entries.get(key) + if existing is None or existing.version != expected_version: + self._stats["cas_conflicts"] += 1 + raise VersionConflict("cache entry version does not match") + return self.set(key, value, ttl_seconds) + + def get_entry(self, key: str) -> CacheEntry | None: + key = self._validate_key(key) + with self._lock: + now = self._clock() + if self._remove_if_expired(key, now): + self._stats["misses"] += 1 + return None entry = self._entries.get(key) if entry is None: - return default - if entry.expires_at is not None and time.monotonic() >= entry.expires_at: - del self._entries[key] - return default - return entry.value + self._stats["misses"] += 1 + return None + self._entries.move_to_end(key) + self._stats["hits"] += 1 + return entry + + def get(self, key: str, default: Any = None) -> Any: + entry = self.get_entry(key) + return default if entry is None else entry.value def delete(self, key: str) -> bool: + key = self._validate_key(key) + with self._lock: + removed = self._entries.pop(key, None) is not None + if removed: + self._stats["deletes"] += 1 + return removed + + def purge_expired(self) -> int: with self._lock: - return self._entries.pop(key, None) is not None + now = self._clock() + expired = [ + key + for key, entry in self._entries.items() + if entry.expires_at is not None and now >= entry.expires_at + ] + for key in expired: + del self._entries[key] + self._stats["expirations"] += len(expired) + return len(expired) - def clear(self) -> None: + def clear(self) -> int: with self._lock: + count = len(self._entries) self._entries.clear() + self._stats["deletes"] += count + return count + + def stats(self) -> dict[str, int | float]: + with self._lock: + self.purge_expired() + result: dict[str, int | float] = dict(self._stats) + result["size"] = len(self._entries) + result["max_entries"] = self._max_entries + requests = self._stats["hits"] + self._stats["misses"] + result["hit_rate"] = ( + self._stats["hits"] / requests if requests else 0.0 + ) + return result def __len__(self) -> int: with self._lock: + self.purge_expired() return len(self._entries) + + +# Backward-compatible alias. The implementation is deliberately single-node; +# the historical repository name must not be interpreted as distributed consensus. +DistributedCache = SkyCache From 576744d8bb0afbc4ef4cd3c3bab8bc4403124f6c Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 18:33:25 -0500 Subject: [PATCH 02/11] feat: expose Sky Cache HTTP sidecar --- main.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 5d622ad..68e4598 100644 --- a/main.py +++ b/main.py @@ -1 +1,151 @@ -print('Starting Python-Distributed-Cache service...')\n \ No newline at end of file +from __future__ import annotations + +import hmac +import json +import os +import time +from typing import Any + +from fastapi import Depends, FastAPI, HTTPException, Request +from pydantic import BaseModel, Field + +from cache import SkyCache, VersionConflict + +SERVICE = "sky-cache" +MAX_VALUE_BYTES = 64 * 1024 + + +def _read_int(name: str, default: int, minimum: int, maximum: int) -> int: + raw = os.getenv(name, str(default)) + try: + value = int(raw) + except ValueError as exc: + raise RuntimeError(f"{name} must be an integer") from exc + if not minimum <= value <= maximum: + raise RuntimeError(f"{name} must be between {minimum} and {maximum}") + return value + + +def _read_default_ttl() -> float | None: + raw = os.getenv("CACHE_DEFAULT_TTL_SECONDS") + if not raw: + return None + try: + value = float(raw) + except ValueError as exc: + raise RuntimeError("CACHE_DEFAULT_TTL_SECONDS must be numeric") from exc + if value < 0: + raise RuntimeError("CACHE_DEFAULT_TTL_SECONDS must be non-negative") + return value + + +MAX_ENTRIES = _read_int("CACHE_MAX_ENTRIES", 10_000, 1, 1_000_000) +API_TOKEN = os.getenv("CACHE_API_TOKEN") or None +if API_TOKEN is not None and len(API_TOKEN) < 16: + raise RuntimeError("CACHE_API_TOKEN must contain at least 16 characters") +cache = SkyCache(default_ttl_seconds=_read_default_ttl(), max_entries=MAX_ENTRIES) +app = FastAPI(title="Sky Cache", version="1.0.0") + + +class SetRequest(BaseModel): + value: Any + ttl_seconds: float | None = Field(default=None, ge=0, le=31_536_000) + expected_version: int | None = Field(default=None, ge=1) + only_if_absent: bool = False + + +def auth(request: Request) -> None: + if API_TOKEN is None: + return + supplied = request.headers.get("authorization", "") + if not hmac.compare_digest(supplied, f"Bearer {API_TOKEN}"): + raise HTTPException(status_code=401, detail="unauthorized") + + +def _validate_value(value: Any) -> None: + try: + payload = json.dumps(value, allow_nan=False, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise HTTPException(status_code=400, detail="value must be JSON serializable") from exc + if len(payload.encode("utf-8")) > MAX_VALUE_BYTES: + raise HTTPException(status_code=413, detail="cache value exceeds 64 KiB") + + +@app.get("/healthz") +def healthz() -> dict[str, str]: + return {"status": "healthy", "service": SERVICE} + + +@app.get("/readyz") +def readyz() -> dict[str, int | str]: + return {"status": "ready", "service": SERVICE, "max_entries": MAX_ENTRIES} + + +@app.get("/metrics") +def metrics() -> dict[str, int | float | str]: + return {"service": SERVICE, **cache.stats()} + + +@app.get("/api/v1/cache/{key}", dependencies=[Depends(auth)]) +def get_value(key: str) -> dict[str, Any]: + entry = cache.get_entry(key) + if entry is None: + raise HTTPException(status_code=404, detail="cache key not found") + ttl_remaining = ( + None + if entry.expires_at is None + else max(0.0, entry.expires_at - time.monotonic()) + ) + return { + "key": key, + "value": entry.value, + "version": entry.version, + "ttl_remaining_seconds": ttl_remaining, + } + + +@app.put("/api/v1/cache/{key}", dependencies=[Depends(auth)]) +def put_value(key: str, request: SetRequest) -> dict[str, Any]: + _validate_value(request.value) + if request.only_if_absent and request.expected_version is not None: + raise HTTPException( + status_code=400, + detail="only_if_absent and expected_version cannot be combined", + ) + try: + if request.only_if_absent: + created, version = cache.set_if_absent( + key, + request.value, + request.ttl_seconds, + ) + if not created: + raise HTTPException(status_code=409, detail="cache key already exists") + elif request.expected_version is not None: + version = cache.compare_and_set( + key, + request.value, + request.expected_version, + request.ttl_seconds, + ) + else: + version = cache.set(key, request.value, request.ttl_seconds) + except VersionConflict as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + except (TypeError, ValueError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"status": "stored", "key": key, "version": version} + + +@app.delete("/api/v1/cache/{key}", dependencies=[Depends(auth)]) +def delete_value(key: str) -> dict[str, Any]: + try: + deleted = cache.delete(key) + except (TypeError, ValueError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return {"status": "deleted" if deleted else "not_found", "key": key} + + +@app.post("/internal/purge-expired", dependencies=[Depends(auth)]) +def purge_expired() -> dict[str, int | str]: + return {"status": "ok", "purged": cache.purge_expired()} From 4841892407f17d047b4b5453cf514eff3d17a6b2 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:07:11 -0500 Subject: [PATCH 03/11] build: modernize Sky Cache runtime dependencies --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 79e57aa..ca1845a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -fastapi==0.103.1\nuvicorn==0.23.2\npydantic==2.3.0\npytest==7.4.2\n \ No newline at end of file +fastapi>=0.115,<1 +uvicorn[standard]>=0.30,<1 +pydantic>=2.9,<3 From b62ca0232c2922f5408164048845bf99d8989df6 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:07:26 -0500 Subject: [PATCH 04/11] test: verify Sky Cache TTL LRU and CAS semantics --- tests/test_cache.py | 81 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/tests/test_cache.py b/tests/test_cache.py index b184f99..e4a45de 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,18 +1,77 @@ +from __future__ import annotations + import pytest -import time -from src.cache import LRUCache -def test_lru_eviction(): - cache = LRUCache(capacity=2, ttl=60) +from cache import SkyCache, VersionConflict + + +class FakeClock: + def __init__(self) -> None: + self.now = 100.0 + + def __call__(self) -> float: + return self.now + + def advance(self, seconds: float) -> None: + self.now += seconds + + +def test_lru_eviction_tracks_capacity() -> None: + clock = FakeClock() + cache = SkyCache(max_entries=2, clock=clock) cache.set("a", 1) cache.set("b", 2) + assert cache.get("a") == 1 cache.set("c", 3) - assert cache.get("a") is None - assert cache.get("b") == 2 + + assert cache.get("b") is None + assert cache.get("a") == 1 assert cache.get("c") == 3 + assert cache.stats()["evictions"] == 1 + + +def test_ttl_expiration_is_deterministic() -> None: + clock = FakeClock() + cache = SkyCache(default_ttl_seconds=10, clock=clock) + cache.set("session", {"user": "sky"}) + assert cache.get("session") == {"user": "sky"} + + clock.advance(10) + assert cache.get("session") is None + assert cache.stats()["expirations"] == 1 -def test_ttl_expiration(): - cache = LRUCache(capacity=10, ttl=1) - cache.set("x", 100) - time.sleep(1.1) - assert cache.get("x") is None + +def test_set_if_absent_reuses_existing_version() -> None: + cache = SkyCache() + created, version = cache.set_if_absent("key", "first") + assert created is True + + created_again, existing_version = cache.set_if_absent("key", "second") + assert created_again is False + assert existing_version == version + assert cache.get("key") == "first" + + +def test_compare_and_set_rejects_stale_version() -> None: + cache = SkyCache() + version = cache.set("counter", 1) + new_version = cache.compare_and_set("counter", 2, version) + assert new_version > version + assert cache.get("counter") == 2 + + with pytest.raises(VersionConflict): + cache.compare_and_set("counter", 3, version) + assert cache.stats()["cas_conflicts"] == 1 + + +def test_validation_and_clear() -> None: + cache = SkyCache(max_entries=3) + with pytest.raises(ValueError): + cache.set("", 1) + with pytest.raises(ValueError): + cache.set("x", 1, ttl_seconds=-1) + + cache.set("a", 1) + cache.set("b", 2) + assert cache.clear() == 2 + assert len(cache) == 0 From d608cb106879df53cf8167767fc3011679edd818 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:07:36 -0500 Subject: [PATCH 05/11] build: add Sky Cache verification dependencies --- requirements-dev.txt | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..9f1ca39 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +-r requirements.txt +pytest>=8,<9 +httpx>=0.27,<1 +ruff>=0.6,<1 +pip-audit>=2.7,<3 From 43d9aac460eacee1aa36587863378d43a23cd6a0 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:07:47 -0500 Subject: [PATCH 06/11] build: harden Sky Cache container runtime --- Dockerfile | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3c39ec..4bde626 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,15 @@ -FROM python:3.11-slim +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -CMD ["python", "-m", "src.server"] +COPY requirements.txt ./ +RUN python -m pip install --upgrade pip && pip install -r requirements.txt \ + && useradd --system --uid 10001 --create-home skycache +COPY cache.py main.py ./ +USER 10001:10001 +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=2).read()" +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080", "--no-access-log"] From b02a4a196652dfc4a3b2b66972079eef6fe37b20 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:09:57 -0500 Subject: [PATCH 07/11] ci: add full Sky Cache verification gate --- .github/workflows/ci.yml | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8c1d31..33ada7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,31 @@ -name: CI -on: [push, pull_request] +name: cache-ci + +on: + push: + pull_request: + +permissions: + contents: read + jobs: - test: + verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.11' - - run: pip install -r requirements.txt - - run: pytest tests/ + python-version: '3.12' + - name: Install verification dependencies + run: python -m pip install --upgrade pip && pip install -r requirements-dev.txt + - name: Compile + run: python -m compileall -q cache.py main.py tests + - name: Lint + run: ruff check cache.py main.py tests + - name: Unit tests + run: pytest -q + - name: Audit runtime dependencies + run: pip-audit -r requirements.txt + - name: Build container + run: docker build -t sky-cache:ci . + - name: Verify non-root image + run: test "$(docker run --rm --entrypoint=id sky-cache:ci -u)" != "0" From 0b69646bca3a73443abdd655a5bbfec96d9b5ae7 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:10:25 -0500 Subject: [PATCH 08/11] docs: align Sky Cache README with verified product --- README.md | 96 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 17c1ede..8194759 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,72 @@ -# Python Distributed Cache +# Sky Cache -Reusable caching component for the SKYCOIN4444 infrastructure ecosystem. +Sky Cache is a bounded, thread-safe, single-process TTL/LRU cache and HTTP service for SKYCOIN4444 infrastructure workloads. -## Current implementation +## Verified capabilities -- Thread-safe in-process cache domain -- TTL expiration using a monotonic clock -- Explicit get/set/delete/clear operations -- Input validation -- Unit tests for expiration, invalidation, and invalid inputs -- FastAPI/uvicorn dependencies retained for the service boundary +- bounded LRU capacity with deterministic eviction +- optional default and per-entry TTL using a monotonic clock +- get, set, delete, clear and expired-entry purge operations +- set-if-absent semantics +- optimistic compare-and-set using monotonically increasing entry versions +- hit/miss, eviction, expiration, mutation and CAS-conflict metrics +- FastAPI service with health, readiness and metrics endpoints +- JSON value limit of 64 KiB +- optional constant-time bearer authentication +- configurable maximum entry count and default TTL +- Python 3.12 non-root container with healthcheck +- CI gates for compile, Ruff, pytest, dependency audit and container user verification -## Ecosystem role +## API -**Infrastructure → Caching Boundary** +- `GET /healthz` +- `GET /readyz` +- `GET /metrics` +- `GET /api/v1/cache/{key}` +- `PUT /api/v1/cache/{key}` +- `DELETE /api/v1/cache/{key}` +- `POST /internal/purge-expired` -The repository currently provides a reusable cache-domain implementation. Despite the repository name, the verified implementation is **not yet a multi-node distributed cache**. Redis/Memcached or another shared backend, consistency semantics, invalidation propagation, and failure handling are still required before that claim is accurate. +`PUT` supports either `only_if_absent=true` or `expected_version=` for optimistic writes. The two controls cannot be combined. -## Commercial starter-kit potential +## Run locally -This component can become an enterprise caching starter kit for: +```bash +python -m pip install -r requirements-dev.txt +pytest -q +uvicorn main:app --host 127.0.0.1 --port 8080 +``` -- API response caching -- session/cache layers -- AI inference/result caching -- market-data caching -- rate-limit state -- frequently accessed platform data +Container: -Its commercial value comes from reusable implementation, tested adapters, deployment configuration, observability, and customer adoption—not from the repository name or “enterprise-grade” wording. +```bash +docker build -t sky-cache . +docker run --rm -p 8080:8080 -e CACHE_MAX_ENTRIES=10000 sky-cache +``` -## Truthful status +Optional authentication: -- Cache domain: **implemented** -- TTL/invalidation tests: **implemented** -- Shared distributed backend: **not integrated** -- Multi-node consistency: **not verified** -- Production deployment: **not verified** -- Paying customers: **not verified** -- ARR/revenue: **not claimed** +```bash +export CACHE_API_TOKEN='replace-with-at-least-16-characters' +``` -The prior README described the project as “enterprise-grade” while the audit evidence showed a small implementation footprint. This README reports the concrete capability instead. fileciteturn296file0 +## Architecture boundary -## Open-source integration policy +Despite the historical repository name `Python-Distributed-Cache`, this product is deliberately **single-process**. It does not implement peer discovery, replication, consensus, cross-node invalidation, persistent storage or Redis protocol compatibility. -For genuine distributed-cache requirements, prefer mature public foundations such as Redis-compatible or other established cache systems rather than inventing distributed consistency and replication protocols. Integrate through a stable adapter and preserve third-party licenses/attribution. +For distributed deployments, place a mature shared cache such as Redis behind an adapter rather than pretending the in-process implementation is distributed. The standalone Sky Cache product remains useful for local service caches, ephemeral inference/result caching, request-local acceleration and test environments. -## Production roadmap +## Security and reliability boundaries -1. Add Redis-backed adapter. -2. Add cache namespace/versioning. -3. Define consistency and invalidation semantics. -4. Add metrics and tracing. -5. Add health/readiness checks. -6. Add integration tests against the target backend. -7. Add load/eviction benchmarks. -8. Add authentication/network controls where required. -9. Package Docker/CI deployment artifacts. -10. Consolidate the strongest implementation into SKYCOIN4444 Infrastructure. +- cache values reside in process memory and are not encrypted at rest because they are not persisted +- bearer authentication is optional and should be enabled when the HTTP service is exposed beyond a trusted boundary +- TLS termination is expected from a trusted reverse proxy or service mesh +- cache contents disappear on process restart +- the service is not a session database or durable financial store +- no multi-node HA or consistency guarantee is claimed + +See `SECURITY.md` and `PRODUCT.md` for deployment and commercial boundaries. ## License -See the checked-in repository license and applicable third-party dependency licenses. +See the checked-in repository license and third-party dependency licenses. From bc9569837f33c2bd6bb9a92c408679ab3c5239a6 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:10:40 -0500 Subject: [PATCH 09/11] docs: define Sky Cache product boundary --- PRODUCT.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 PRODUCT.md diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..d87aafb --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,24 @@ +# Sky Cache Product Definition + +**Product number:** 13 in the SKYCOIN4444 standalone-product master plan. + +## Problem + +Applications often need a bounded local cache with predictable TTL, eviction and optimistic-update semantics without operating an external cache service for every workload. + +## Product + +Sky Cache packages a reusable Python cache domain and an optional HTTP service. It is suitable for ephemeral API response caching, local AI inference/result caching, short-lived metadata, development/test acceleration and other non-durable workloads. + +## Commercial packaging + +- embeddable `SkyCache` Python class +- standalone FastAPI service +- non-root container image +- health/readiness/metrics endpoints +- optional bearer boundary +- deterministic test suite and dependency audit CI + +## Explicit non-claims + +Sky Cache is not Redis, Memcached, a persistent database, a distributed cache, a consensus system, a multi-region service or a durable session/financial store. Those capabilities require a separate shared backend or product tier and independent evidence. From d15e262ca6a02dd0024c036a9b41b0c51940d75f Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:10:53 -0500 Subject: [PATCH 10/11] docs: document Sky Cache security model --- SECURITY.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..06013ee --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +## Supported product boundary + +Sky Cache stores values only in the memory of the current process. It should be treated as an ephemeral cache, not as a secret store or durable database. + +## Deployment requirements + +- enable `CACHE_API_TOKEN` when exposing the HTTP boundary outside a trusted network +- terminate TLS at a trusted reverse proxy or service mesh +- do not cache plaintext credentials, private keys, regulated data, or other material that requires durable encryption controls +- apply network-level access control and resource limits in the deployment environment +- restart the service to clear all cached values when a full purge is required + +## Implemented controls + +- bounded key length and value size +- bounded maximum entry count +- bounded TTL +- constant-time bearer comparison when authentication is configured +- optimistic version checks for write races +- non-root container execution +- dependency auditing in CI + +## Not implemented + +Sky Cache does not provide tenant isolation, encryption at rest, distributed authentication, TLS termination, replication, consensus, cross-node invalidation, or durable audit logging. + +Report suspected vulnerabilities through the repository's GitHub security/reporting channel without including live secrets in public issues. From 84bfb0f0751b490581c47b90d5b1d8381196edb1 Mon Sep 17 00:00:00 2001 From: Skyler Blue Spillers <92972770+skylerblue333@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:13:24 -0500 Subject: [PATCH 11/11] ci: expose Sky Cache root module to tests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ada7a..a002475 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ permissions: jobs: verify: runs-on: ubuntu-latest + env: + PYTHONPATH: . steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5