Repository navigation
Repo review: a silent captcha bypass, three doubling generators, and 246 lines that lied - #226
Conversation
`get_secret` prefers a mounted Docker secret file over the environment, and its docstring says why: a deployment that has gone to the trouble of mounting a secret means it. bin/chat-fastapi.py loads CLOUDFLARE_SECRET_KEY that way at import, and then asked `os.environ` twice -- once to decide whether the captcha middleware runs at all, once to pick the secret sent to Cloudflare. So a deployment that mounts the key instead of exporting it got the captcha silently switched off: the middleware saw an empty environment variable and returned early, skipping a protection the operator had configured. If it were reached anyway, Cloudflare would be sent a null secret and the failure would read as a captcha problem rather than a configuration one. Both sites now use the resolved value. This matters now rather than later because init-docker-secrets on the security-improvements branch mounts secrets exactly this way, so the bug would have arrived with that work. Tests pin both halves: that get_secret really does prefer the mounted file and that os.environ cannot see it, and a source-level guard that the script never reaches around get_secret for this secret -- checked by reintroducing the bug, which fails the guard. The guard is source-level because the script is hyphenated and therefore not importable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`upload_to_chromadb` was 246 lines and most of it was misleading. It listed the CSV directory and printed each filename, doing nothing with them. It carried five stray debug prints, one of which printed a loop variable after the loop. It built its path as the literal "./csv_files/alliance/<version>/", so it worked from the repo root and nowhere else. And it iterated seven filetypes while an `if filetype == "genes"` inside the loop meant only one was ever built. The function is 56 lines now. The seven column schemas move to module scope as COLUMN_SCHEMAS, because they are data: 203 lines of them inside the body made a 40-line routine read as a 246-line one. What is deliberately NOT removed: those six unused schemas. They are curated MITAB and Alliance column lists, and tests/data_generation/test_alliance_columns.py exists because a missing comma once silently concatenated string literals and collapsed seven molecular_interaction columns into three. Deleting them would have thrown away that work and its guard -- which I started to do before the test caught me. Recorded instead of assumed: the schemas cannot simply be switched on. The downloader writes variants_c_elegans.tsv, variants_zebrafish.tsv and so on, while the schema key is "variants", so that entry could never have matched a file even without the genes-only guard. Two hand-maintained lists of the same thing, already disagreeing, with nothing to notice -- the comment now says so. Also: a missing file is now logged and skipped rather than raising from inside Chroma, and regeneration removes the collection first, since Chroma.from_documents appends and silently doubled the reactome bundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
uniprot had no guard at all, and userguide removed its collection only under `force` -- but `force` governs whether the HTML pages are re-fetched, which is a different question from whether the collection is rebuilt from them. Either way an ordinary second run appended, storing every document twice with no error and nothing in the output to suggest it. That makes three of the five generators, each written separately and each carrying the same fault. The shipped Release95 reactome bundle is what it looks like in practice: 33,498 reaction documents for 16,749 CSV rows, unnoticed until the bundle was rebuilt, with the vector retriever spending half its over-fetch on duplicates of what it already had. The real fix is one shared persist helper instead of five hand-written copies. That is deliberately not done here: reactome, uniprot and userguide have no executing tests at all, and alliance's only test parses the AST. Extracting across five untested modules is how a fault like this gets introduced rather than removed. So this adds a source-level guard over all five first -- checked by reintroducing the bug in uniprot, which fails it -- and the extraction becomes a change something would catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It wrote and read "./csv_files/alliance/<version>/", relative to the process's working directory. The two halves agreed only because both were relative in the same way, so it worked from the repo root and silently found nothing anywhere else. Every other generator derives its paths from embeddings_dir; this one now does too, and the directory is passed to the downloader rather than assumed by both ends. Checked across src/: alliance held the only working-directory-relative paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two faults in yesterday's review, found by attacking it. The guard I wrote asserted that `rmtree` and `clear_system_cache` appear in each generator's source. That passes when the code removes a completely unrelated directory -- checked, and it did pass. It was asserting the shape of the code rather than its behaviour, which is the thing this project keeps catching elsewhere and I did it in the detector itself. The tests now build each collection twice with a fake embedding and assert the count does not double. Checked against both mutations: removing the guard entirely, and removing the *wrong* directory. The old test caught only the first; this catches both. userguide stays source-level because generating it fetches pages over the network, and that is said in the test rather than left to be discovered. Second fault: `parent_dir: str = "."` on the Alliance CSV functions. The whole point of that change was to stop writing relative to the working directory, and a default of "." silently restores exactly that for any caller who forgets the argument. It is required now, so forgetting it is a TypeError rather than files appearing somewhere nobody looks. A new generator without a case in this file now fails a test, because five hand-written persist blocks is why the same fault appeared three times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial review of this PR — two faults, both mineThe guard test asserted shape, not behaviour. It checked that Replaced with tests that build each collection twice with a fake embedding and assert the count doesn't double. Verified against both mutations:
Also added: a new generator without a case in this file fails a test. Five hand-written persist blocks is why the same fault appeared three times. What held up: the captcha fix (traced again — behaviour is identical when the secret is in the environment or absent, and differs only in the mounted-file case it exists to fix), the Alliance refactor, and the finding that Alliance held the only working-directory-relative paths in 326 tests, ruff and mypy clean. 🤖 Generated with Claude Code |
An adversarial pass over the whole repo. Four findings, all verified, each pinned by a test that fails when the bug is put back.
1. A deployment that mounts the captcha secret gets no captcha
get_secretprefers a mounted Docker secret file over the environment, and says why: "a deployment that has gone to the trouble of mounting a secret means it".bin/chat-fastapi.pyloadsCLOUDFLARE_SECRET_KEYthat way — then askedos.environtwice, once to decide whether the captcha middleware runs at all and once to pick the secret sent to Cloudflare.So mounting the key instead of exporting it silently switched the captcha off: the middleware saw an empty environment variable and returned early. If it were reached anyway, Cloudflare got a
Nonesecret and the failure would read as a captcha problem rather than a configuration one.This matters now rather than later because
init-docker-secretson the security-improvements branch mounts secrets exactly this way — the bug would have arrived with that work. Audited the rest: this was the only such divergence.2. Three of five generators doubled their collections
Chroma.from_documentsappends. Running a build twice stores every document twice, with no error.That isn't hypothetical — the shipped Release95 bundle had 33,498 reaction documents for 16,749 CSV rows, exactly 2.00x, unnoticed until the Release 97 rebuild. The vector retriever over-fetches then de-duplicates on
st_id, so half that over-fetch was spent on duplicates.uniprotuserguideforce— butforcemeans re-fetch the HTML, not rebuild the collectionreactome,alliance,disease_variantThe real fix is one shared persist helper, and this PR deliberately does not do it.
reactome,uniprotanduserguidehave no executing tests, andalliance's only test parses the AST. Extracting across five untested modules is how a fault like this gets introduced rather than removed. So there's now a source-level guard over all five, and the extraction becomes a change something would catch.3. The Alliance generator was 246 lines and most of it was misleading
It listed the CSV directory and printed every filename, using none of them. Five stray debug prints, one printing a loop variable after the loop. And it iterated seven filetypes while an
if filetype == "genes"inside the loop meant only one was ever built.56 lines now, with the seven column schemas lifted to module scope, because they are data — 203 lines of them inside the body made a 40-line routine read as a 246-line one.
What is deliberately kept: the six unused schemas. They're curated MITAB column lists, and
test_alliance_columns.pyexists because a missing comma once concatenated string literals and collapsed sevenmolecular_interactioncolumns into three. I started to delete them and that test stopped me.Recorded rather than assumed: they can't just be switched on. The downloader writes
variants_c_elegans.tsv,variants_zebrafish.tsv…, while the schema key is"variants"— so that entry could never have matched a file even without the genes-only guard. Two hand-maintained lists of the same thing, already disagreeing, with nothing to notice.4. Alliance was the only module using working-directory-relative paths
It wrote and read
"./csv_files/alliance/<version>/". Both halves were relative in the same way, so it worked from the repo root and silently found nothing anywhere else. Now derived fromembeddings_dirlike every other generator, and passed to the downloader rather than assumed at both ends.326 tests, ruff and mypy clean.
🤖 Generated with Claude Code