Files
BlackVortexAIandClaude Fable 5.1 ac5150e856 Move trusted runtime data into ComfyUI's private System User directory
Remote LLM settings and secrets, the response cache, LUT working catalogs
and channel selection, and the LoRA header cache now live below
user/__bv_nodepack/, which ComfyUI never serves through /userdata. Files
from the public user/default/bv_nodepack/ tree are migrated once at
startup: verified copy, then removal, never overwriting private state.

Without the System User API the pack fails closed: no public fallback,
API-key storage and custom bearer providers refuse with a clear message,
LUT catalogs are served from the bundled files read-only, caches stay in
memory. Fixed-catalog and local providers keep working.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 15:39:04 +02:00

101 lines
4.2 KiB
Python

"""Disposable header evidence cache; metadata and previews are never cached here."""
import json
import os
from pathlib import Path
import tempfile
# Bump when header validation or canonical target mapping changes.
HEADER_CHECK_VERSION = 1
MAX_BYTES = 8 * 1024 * 1024
MAX_ENTRIES = 10000
def default_header_cache_path(folder_paths):
# Private System User storage only; None disables persistence rather than
# writing into the HTTP-served user/default tree.
from .user_storage import private_path
return private_path("cache", "lora_header_checks.json", folder_paths_module=folder_paths)
def _valid_result(value):
return (isinstance(value, dict) and set(value) == {"status", "family", "reason", "target_model"}
and value["status"] in ("unknown", "token_candidate", "multipass")
and value["family"] in (None, "Krea 2") and value["target_model"] == "unknown"
and isinstance(value["reason"], str) and len(value["reason"]) <= 2048)
class HeaderCheckCache:
"""One catalog request owns an index. Atomic replacement prevents torn writes.
Concurrent requests may lose newly cached entries, causing harmless rechecks.
Cache signatures use canonical path, size and nanosecond modification time.
"""
def __init__(self, path, *, algorithm_version=HEADER_CHECK_VERSION):
self.path = Path(path) if path else None
self.version = algorithm_version
self.entries = {}
self.dirty = False
if self.path is None:
return
try:
with self.path.open("rb") as stream:
raw = stream.read(MAX_BYTES + 1)
if len(raw) > MAX_BYTES:
return
data = json.loads(raw)
if (not isinstance(data, dict) or set(data) != {"version", "entries"}
or type(data["version"]) is not int or data["version"] != self.version
or not isinstance(data["entries"], dict) or len(data["entries"]) > MAX_ENTRIES):
return
for key, entry in data["entries"].items():
if (not isinstance(key, str) or len(key) > 32768 or not isinstance(entry, dict)
or set(entry) != {"size", "mtime_ns", "result"}
or any(type(entry[name]) is not int or entry[name] < 0 for name in ("size", "mtime_ns"))
or not _valid_result(entry["result"])):
return
self.entries = data["entries"]
except (OSError, ValueError, UnicodeError, RecursionError):
pass
def resolve(self, path, checker):
path = Path(path)
stat = path.stat()
key = os.path.normcase(str(path.resolve()))
signature = {"size": stat.st_size, "mtime_ns": stat.st_mtime_ns}
previous = self.entries.get(key)
if previous and all(previous[name] == value for name, value in signature.items()):
return previous["result"].copy()
result = checker(path)
after = path.stat()
if (self.path is not None and _valid_result(result)
and (after.st_size, after.st_mtime_ns) == (stat.st_size, stat.st_mtime_ns)):
if key not in self.entries and len(self.entries) >= MAX_ENTRIES:
self.entries.pop(next(iter(self.entries)))
self.entries[key] = {**signature, "result": result.copy()}
self.dirty = True
return result
def save(self):
if self.path is None or not self.dirty:
return
temporary = None
try:
payload = json.dumps({"version": self.version, "entries": self.entries}, separators=(",", ":")).encode()
if len(payload) > MAX_BYTES:
return
self.path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(dir=self.path.parent, prefix=".lora-header-", delete=False) as stream:
temporary = Path(stream.name)
stream.write(payload)
os.replace(temporary, self.path)
self.dirty = False
except OSError:
pass
finally:
if temporary is not None:
try:
temporary.unlink(missing_ok=True)
except OSError:
pass