Skip to content

Result retention controls - #15

Merged
FWao merged 3 commits into
mainfrom
result-retention-controls
Aug 6, 2026
Merged

Result retention controls#15
FWao merged 3 commits into
mainfrom
result-retention-controls

Conversation

@FWao

@FWao FWao commented Aug 6, 2026

Copy link
Copy Markdown
Member

No description provided.

FWao added 3 commits August 6, 2026 14:42
The NEJM AI paper (Wiest et al. 2025, doi:10.1056/AIdbp2400537) established
the approach this app builds on, so it belongs in the citation metadata — as
a `references:` entry, with the software itself still the primary citation.

Not `preferred-citation`: that would make GitHub's "Cite this repository"
button emit the paper instead of the software, attributing this codebase to
a study that evaluated a different one. The reference's `notes:`, the README
and the docs landing page all say the same thing in prose, since most people
never open the .cff — this is a follow-up implementation, and the accuracy
reported in the paper does not describe this tool.
A security review of the request cache turned up three ways a document
stayed reachable longer than it should, and one place it reached a disk:

- nginx spools request bodies over client_body_buffer_size (16k!) and
  responses over the proxy buffers to /tmp, which in the frontend container
  is the writable layer. Every upload and every anonymize response was
  therefore written to disk, contradicting "nothing touches disk" in
  DATA_RETENTION.md. /tmp is now a tmpfs, the body buffer is 1 MB and
  /api/ responses stream through unbuffered.
- The cache TTL was refreshed on every read, so "≤15 minutes" was idle
  time, not maximum age, and eviction only ran inside put/get — an idle
  process held its last documents indefinitely. The TTL is now absolute
  from creation, a lifespan task sweeps every minute, and shutdown clears
  the cache.
- The request id is the only credential for a cached document (POST
  /anonymize with {request_id, overrides} returns the full source text),
  and it was written to the INFO log on every request. It is now in
  FORBIDDEN_FIELDS; the endpoints log ref=log_reference(id) instead.

Also: DELETE /anonymize/{id}, called when a document is closed, reset or
the tab unloads, so the server-side copy ends when the user is done rather
than at the end of a TTL; and the export path discards the entry its own
cache-miss run created, which no client could ever reference.

Since results now expire on a fixed schedule, the UI has to say so. The
header carries a countdown that doubles as the extend button — a press
buys a full extension window from that moment, repeatably, so stepping
away from the desk does not cost a re-run — and the result view warns
separately in the last minute. The lifetime is batch-wide: it counts down
to whichever document expires first and one press extends them all.

The durations are RESULT_CACHE_TTL_MINUTES (15),
RESULT_CACHE_EXTENSION_MINUTES (60), RESULT_CACHE_MAX_LIFETIME_MINUTES
(720) and RESULT_CACHE_MAX_ENTRIES (100). The defaults suit a research
prototype; the ceiling is what makes the retention statement true, and
setting it equal to the TTL turns extending off. Misconfiguration fails
toward less retention: a ceiling below the TTL shortens the first window.

Durations read as "1 Std. 5 Min." / "45 Min." / "47 Sek." via Intl unit
formatting — m:ss showed "60:00" after an extension and implied a
precision nobody needs at that range.
The changelog is read by users and by the admin setting the app up, and the
0.1.0 section had grown to roughly 75 lines of sub-headed bullets while the
unreleased entries carried the rationale and migration notes that belong on
a docs/ page. Both are condensed: one bullet per change, naming the setting
or route that moved and linking to the page that explains it. The retention
entry went from ten lines to five, 0.1.0 from four subsections to eleven
bullets. Nothing was dropped that a user or an operator would act on.

The unreleased entries become 0.1.1, dated today, with the compare links.
Version bumped in package.json, pyproject.toml, CITATION.cff, the
APP_VERSION default, both lockfiles, the THIRD_PARTY_NOTICES header and the
DEIDENTIFIER_IMAGE_TAG examples in the deployment docs.

AGENTS.md already said to keep the changelog short, and that clearly was not
enough to prevent the drift, so the rule is now checkable rather than a
sentiment: one bullet, one or two sentences, no sub-bullets, no rationale,
no migration notes, and a release section that fits on a screen (~20 lines).
Condensing is step 3 of Releasing, including the older sections, rather than
optional cleanup. Step 2 was also wrong — it called `uv lock` conditional on
dependency changes, but both lockfiles carry the project version, so every
bump needs them, along with the two version strings that live in prose.
@FWao
FWao merged commit 250aa1b into main Aug 6, 2026
2 of 9 checks passed
@FWao
FWao deleted the result-retention-controls branch August 6, 2026 16:09
first = client.post("/api/v1/anonymize", json={"text": SAMPLE_TEXT}).json()
request_id = first["request_id"]

assert client.delete(f"/api/v1/anonymize/{request_id}").status_code == 204
def test_delete_of_an_unknown_id_reveals_nothing(client):
"""Same answer either way — whether an id exists is not something an
unrelated caller should be able to probe."""
assert client.delete("/api/v1/anonymize/no-such-id").status_code == 204
Comment thread backend/src/main.py
finally:
sweeper.cancel()
with suppress(asyncio.CancelledError):
await sweeper
"""Whatever the UI does, a document leaves memory at the configured
maximum. Set to 30 minutes here so the test does not have to simulate a
12-hour day."""
import backend.src.utils.cache as cache_module
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants