From b2cd434a024639080e19120d1ec7ae0d513ee9f9 Mon Sep 17 00:00:00 2001 From: man4ish Date: Sun, 4 Oct 2026 03:40:19 -0500 Subject: [PATCH] fix: cap V1_IDEMPOTENCY_TTL at 30 days (design audit gap #11) V1Store's idempotency-replay cache holds the real answer text and citations returned by /v1/literature/answers, not just metadata -- one of only two places in this system that text survives at all (the other is omnibioai-rag's own query cache). The design doc's 30-day question/answer deletion policy is enforced here as a hard ceiling on this already-short-lived cache's own TTL (default 24h), not a separate deletion job -- there is no durable store for one to run against. Co-Authored-By: Claude Sonnet 5 --- app/core/config.py | 10 +++++++++- tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/core/config.py b/app/core/config.py index 8c79b69..126ea18 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -56,7 +56,15 @@ class Config: # omnibioai-billing are then not visible. V1_REDIS_URL = os.getenv("V1_REDIS_URL", "") V1_RATE_LIMIT_PER_MINUTE = int(os.getenv("V1_RATE_LIMIT_PER_MINUTE", "60")) - V1_IDEMPOTENCY_TTL = int(os.getenv("V1_IDEMPOTENCY_TTL", "86400")) + # M18 (design audit gap #11): this cached replay body is the real + # answer text/citations for /v1/literature/answers, not just + # metadata -- one of only two places that text survives anywhere in + # this system at all (the other is omnibioai-rag's own query + # cache). The design doc's 30-day question/answer deletion policy + # is enforced here as a hard ceiling on this already-short-lived + # cache's TTL, not a separate deletion job -- there is no durable + # store for one to run against. + V1_IDEMPOTENCY_TTL = min(int(os.getenv("V1_IDEMPOTENCY_TTL", "86400")), 30 * 24 * 60 * 60) # Design audit gap #7 ("concurrent-answer limits are absent"): the # most expensive /v1 call (it invokes an LLM, up to RAG's own # 300-second timeout) is capped on how many of a single key's or diff --git a/tests/test_config.py b/tests/test_config.py index f10aa45..c785867 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -41,6 +41,30 @@ def test_config_reads_iam_url_from_env(monkeypatch): importlib.reload(cfg_module) +def test_v1_idempotency_ttl_is_capped_at_30_days_even_if_configured_higher(monkeypatch): + """M18 (design audit gap #11): the idempotency-replay cache holds + the real answer text/citations, so its TTL is the de facto + enforcement of the 30-day deletion policy -- a misconfigured env + var must not be able to silently violate that.""" + monkeypatch.setenv("V1_IDEMPOTENCY_TTL", str(60 * 24 * 60 * 60)) # 60 days + import app.core.config as cfg_module + + importlib.reload(cfg_module) + assert cfg_module.Config.V1_IDEMPOTENCY_TTL == 30 * 24 * 60 * 60 + monkeypatch.delenv("V1_IDEMPOTENCY_TTL", raising=False) + importlib.reload(cfg_module) + + +def test_v1_idempotency_ttl_below_the_30_day_cap_is_unaffected(monkeypatch): + monkeypatch.setenv("V1_IDEMPOTENCY_TTL", "60") + import app.core.config as cfg_module + + importlib.reload(cfg_module) + assert cfg_module.Config.V1_IDEMPOTENCY_TTL == 60 + monkeypatch.delenv("V1_IDEMPOTENCY_TTL", raising=False) + importlib.reload(cfg_module) + + def test_config_defaults_are_set(): """IAM_URL/REDIS_URL/JWT_SECRET all have non-empty defaults, so the gateway can start with no environment variables configured."""