Skip to content

Merge pull request #47 from lamalab-org/main - #55

Merged
r-fedorov merged 12 commits into
mainfrom
tests
Jul 24, 2026
Merged

Merge pull request #47 from lamalab-org/main#55
r-fedorov merged 12 commits into
mainfrom
tests

Conversation

@AdrianM0

@AdrianM0 AdrianM0 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added a web interface for drawing molecules and receiving IUPAC names.
    • Added molecule descriptions, structure depictions, copy-to-clipboard, example presets, and optional OPSIN verification.
    • Added health and warmup endpoints for improved service readiness.
  • Improvements

    • Expanded naming support for complex substituents, esters, charged sulfur structures, amido groups, and fused derivatives.
    • Improved stereochemical naming for small rings.
    • Improved response speed through caching and optimized analysis.
  • Bug Fixes

    • Corrected several carbonyl, hydroxy, contraction, and locant formatting issues.

r-fedorov and others added 12 commits July 22, 2026 15:48
* add the modern CIP labeller

* fix path

* push fix

* push additional stereo audit

* feat: add complete audit

* ruff

* push website code

* website: default OPSIN verify, squared redesign, atom-id depiction, version tag

- OPSIN round-trip verification is now on by default
- flat/squared restyle: no rounded corners, left-aligned header
- long names and atom-id lines wrap instead of overflowing
- new /api/depict endpoint renders the molecule with RDKit atom
  indices; shown in the "Explain this name" section
- pin openclatura[web]==0.1.4 and show the installed version in the
  header (healthz override reports distribution metadata, since the
  0.1.4 wheel hardcodes __version__ = "0.1.0")

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: bundle X11 libs so rdkit depiction works on Vercel

rdkit.Chem.Draw dlopens a bundled libcairo that needs libXrender &
friends, absent from the Vercel runtime image. Bundle the closure
(Debian bullseye builds, glibc<=2.26 symbols, compatible with the
AL2023 runtime) and ctypes-preload them before the Draw import.
The import is now also non-fatal: without it /api/depict degrades
instead of crashing every endpoint at cold start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: add libexpat to bundled X11 libs

fontconfig inside the rdkit wheel needs libexpat.so.1, also absent
from the Vercel runtime image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: serialize OPSIN verification to fix intermittent errors

py2opsin writes a fixed-name temp file in the CWD; concurrent requests
in one warm function process collide on it and the round-trip raises,
surfacing as status "error". Shadow /api/name with a version that holds
a process-wide lock around verify_opsin runs and retries once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: cache naming/describe/depict results in Upstash Redis

Results are deterministic per package version, so responses are cached
under {endpoint}:{version}:{flags}:{sha256(canonical SMILES)} with a
90-day TTL via the Upstash REST API (stdlib urllib, no new deps).
Failures and OPSIN error/skipped statuses are never cached; any cache
error falls back to computing. Without KV_REST_API_URL/TOKEN the cache
is a no-op, so the deploy is safe before the store is provisioned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: add S3-compatible cache backend (own bucket)

The result cache can now use any S3-compatible object store via
stdlib SigV4 signing (CACHE_S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY;
AWS_* names accepted for local runs but reserved on Vercel). Upstash
REST remains as fallback backend. Expiry via bucket lifecycle rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: persistent OPSIN JVM per instance

OPSIN's CLI streams names line-by-line, so keep one JVM alive per
function instance instead of booting one per verification (~1.5 s
-> ~10 ms warm). Comparison logic mirrors verify_with_opsin; on any
daemon hiccup the process is killed and the request falls back to
the one-shot py2opsin path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: add stdlib-only cache size report script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: warm backend while Ketcher loads

New GET /api/warmup boots the OPSIN JVM and primes the naming engine;
the frontend fires it on page load so the serverless cold start
overlaps with editor initialization instead of delaying the first
naming request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* website: fix white editor backdrop in dark mode

Ketcher themes its own canvas, so the hardcoded white .editor-shell
background showed as a sliver around the iframe (and the loading
overlay flashed white) for dark-mode visitors. Drive both from a
--editor-bg variable instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* website: wait 1.5s after the last edit before naming

600ms fired while people were still drawing, naming half-finished
structures (and spending a request on each). Raise the edit debounce
to 1.5s behind a named constant, show "waiting for you to finish
drawing…" while it counts down, and clear that status on edits that
don't change the structure. Example chips keep their short wait since
they load complete molecules.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* website: 900ms debounce, and don't flash errors mid-drawing

The 1.5s wait was masking the real problem: a failed intermediate
naming wiped the panel and showed a red error, so any request on a
half-drawn structure looked broken. Instead:

- keep the last good name on screen (dimmed) while a request is in
  flight, rather than blanking the panel
- hold naming failures behind a 2s grace window that any further edit
  cancels, so only a drawing the user has stopped touching reports an
  error
- with failures no longer jarring, drop the debounce to 900ms

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert to main

* drop irrelevant files for this branch

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
speed-up computation 2x (add caching)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @AdrianM0, your pull request is larger than the review limit of 300000 diff characters

@AdrianM0
AdrianM0 requested a review from r-fedorov July 24, 2026 10:45
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change revises naming assembly and analysis internals, introduces request-scoped token-span control and caching, removes legacy wrappers, and adds a Vercel-hosted Ketcher interface with cached naming, description, depiction, warmup, health, and OPSIN verification endpoints.

Changes

Naming engine changes

Layer / File(s) Summary
Naming output and modifier assembly
src/openclatura/assembler.py, src/openclatura/namer.py, src/openclatura/component_*.py, src/openclatura/small_ring_stereo.py, src/openclatura/tests/test_analysis.py, tests/integration/test_describer.py
Legacy rewrites, amido naming, locanted front modifiers, charged sulfur naming, retained-fused gating, stereo behavior, and related expectations are updated.
Ring and perception analysis caching
src/openclatura/chains.py, src/openclatura/molecule.py, src/openclatura/perception.py, src/openclatura/von_baeyer.py
Ring and perceived-group calculations reuse molecule caches that are invalidated on graph mutation; obsolete ring helpers are removed.
Typed assembly and request-scoped metadata
src/openclatura/name_assembly.py, src/openclatura/name_operations.py, src/openclatura/naming_context.py, src/openclatura/engine.py, src/openclatura/*_naming.py, src/openclatura/special_cases.py
Typed parent assembly state and HydroOperation replace removed structures, token spans become optional per request, and compatibility wrappers are removed.

Website application

Layer / File(s) Summary
Serverless API and deployment wiring
website/api/index.py, website/requirements.txt, website/vercel.json, website/scripts/cache_size.py, website/.gitignore
A Vercel FastAPI entrypoint adds cached naming, description, depiction, warmup, health, and daemon-backed OPSIN verification routes.
Editor-facing naming interface
website/public/index.html
The page embeds Ketcher, submits debounced naming requests, displays rule and OPSIN status, and loads explanations and depictions.
Embedded Ketcher distribution
website/public/ketcher/*
Ketcher manifests, route rewrites, compiled assets, bundles, and license files are added.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: r-fedorov

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is a generic merge message and does not describe the substantive changes in the pull request. Replace it with a concise summary of the main change, such as the new web API and frontend integration for naming molecules.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.44.1)
website/public/ketcher/static/js/main.cb80d824.js

ast-grep skipped this file: it is too large to scan (29521424 bytes)

website/public/ketcher/static/js/535.c5fc3b49.chunk.js

ast-grep timed out on this file

website/public/ketcher/static/js/622.ed91acd0.chunk.js

ast-grep timed out on this file


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (2)
src/openclatura/assembler.py (1)

148-233: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a regression test covering the heteroaryl-methyl-in-group contraction.

The paren-aware helper avoids the inner )methyl false match in (oxo)(1-((thiophen-2-yl)methyl)cyclohexyl)methyl, but no direct regression test exercises this exact failure mode.

Suggested test to add
def test_oxo_group_methyl_to_carbonyl_handles_nested_methyl_group():
    assert _oxo_group_methyl_to_carbonyl(
        "(oxo)(1-((thiophen-2-yl)methyl)cyclohexyl)methyl"
    ) == "(1-((thiophen-2-yl)methyl)cyclohexyl)carbonyl"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openclatura/assembler.py` around lines 148 - 233, The paren-aware
_oxo_group_methyl_to_carbonyl helper lacks regression coverage for nested
heteroaryl-methyl groups. Add a focused test for _oxo_group_methyl_to_carbonyl
using "(oxo)(1-((thiophen-2-yl)methyl)cyclohexyl)methyl" and assert it returns
"(1-((thiophen-2-yl)methyl)cyclohexyl)carbonyl".
website/api/index.py (1)

140-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated AWS SigV4 signing implementation across two files. Both files independently hand-roll the same SigV4 canonical-request/signing-key derivation (deliberately avoiding boto3 to keep the Vercel bundle small), so a correctness fix or algorithm change in one would need to be manually mirrored in the other.

  • website/api/index.py#L140-L186: extract the SigV4 signing steps (canonical request, scope, derived key, signature) into a small shared stdlib-only helper module importable by both this file and the script.
  • website/scripts/cache_size.py#L30-L63: replace s3_get's inline signing logic with the same shared helper, keeping only the GET-with-query-string specifics local to this script.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/api/index.py` around lines 140 - 186, Extract the shared AWS SigV4
canonical-request, credential-scope, signing-key, and signature logic from
_s3_request in website/api/index.py (lines 140-186) into a small stdlib-only
helper importable by both files. Update website/scripts/cache_size.py (lines
30-63) to replace s3_get’s duplicated signing implementation with that helper,
retaining only its GET query-string handling locally; update _s3_request to use
the same helper as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openclatura/name_assembly.py`:
- Around line 15-28: The global _BUILD_TOKEN_SPANS toggle in
set_token_span_building is unsafe for concurrent NamingEngine.run requests.
Replace it with thread/task-local request context, ensure
set_token_span_building preserves and returns the caller’s prior context value,
and update the token-span construction path to read that context so each request
independently controls span building.

In `@src/openclatura/namer.py`:
- Around line 1238-1245: Update the amido prefix concatenation in the
n_substituents branch to insert a hyphen when amido begins with either a digit
or a parenthesized stereo descriptor, while preserving the existing no-hyphen
behavior for other prefixes. Use the existing amido and _format_n_locant_prefix
logic without changing downstream complex-prefix formatting.

In `@website/api/index.py`:
- Around line 406-419: Update describe_endpoint to handle exceptions raised by
describe(req.smiles), returning the same {"ok": False, "error": ...} structured
response used by depict for unparsable SMILES instead of propagating a generic
500. Preserve the existing cache lookup and successful-response caching
behavior.
- Around line 249-403: Extend the OPSIN concurrency protection beyond
name_endpoint: ensure name_many batch execution with processes != 1 cannot
invoke verify_with_opsin/py2opsin concurrently across worker processes. Prefer
routing verification through a shared serialized daemon, or disable/offload
verification for that multi-process path while preserving existing behavior for
single-process and non-batch requests.
- Around line 58-90: Harden both tar extraction sites in _preload_xlibs and
_ensure_java by validating archive member paths before extraction, rejecting any
entry that resolves outside the staging directory. Apply the same
safe-extraction approach to both tar.extractall calls while preserving their
existing staging and cleanup behavior.

In `@website/public/index.html`:
- Around line 368-372: Version asynchronous editor work by a shared revision
counter. In website/public/index.html:368-372, increment the revision on every
Ketcher change before scheduling naming; in website/public/index.html:403-418,
capture the revision and validate it after getSmiles() before calling
requestName(); and in website/public/index.html:534-599, capture and validate
the revision before rendering explanation or depiction results. Ensure stale
completions cannot mutate the UI.

In `@website/requirements.txt`:
- Around line 1-2: Add an explicit h11>=0.16.0 constraint to
website/requirements.txt alongside the existing package specifications, while
leaving the openclatura and py2opsin entries unchanged.

---

Nitpick comments:
In `@src/openclatura/assembler.py`:
- Around line 148-233: The paren-aware _oxo_group_methyl_to_carbonyl helper
lacks regression coverage for nested heteroaryl-methyl groups. Add a focused
test for _oxo_group_methyl_to_carbonyl using
"(oxo)(1-((thiophen-2-yl)methyl)cyclohexyl)methyl" and assert it returns
"(1-((thiophen-2-yl)methyl)cyclohexyl)carbonyl".

In `@website/api/index.py`:
- Around line 140-186: Extract the shared AWS SigV4 canonical-request,
credential-scope, signing-key, and signature logic from _s3_request in
website/api/index.py (lines 140-186) into a small stdlib-only helper importable
by both files. Update website/scripts/cache_size.py (lines 30-63) to replace
s3_get’s duplicated signing implementation with that helper, retaining only its
GET query-string handling locally; update _s3_request to use the same helper as
well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e291da34-3cc0-4cbc-87b5-40a704b740c9

📥 Commits

Reviewing files that changed from the base of the PR and between 8fe914e and e006cda.

⛔ Files ignored due to path filters (7)
  • website/api/jre.tar.gz is excluded by !**/*.gz
  • website/api/xlibs.tar.gz is excluded by !**/*.gz
  • website/public/ketcher/apple-touch-icon.png is excluded by !**/*.png
  • website/public/ketcher/favicon-16x16.png is excluded by !**/*.png
  • website/public/ketcher/favicon-32x32.png is excluded by !**/*.png
  • website/public/ketcher/favicon.ico is excluded by !**/*.ico
  • website/public/ketcher/logo.svg is excluded by !**/*.svg
📒 Files selected for processing (48)
  • src/openclatura/assembler.py
  • src/openclatura/assembly_parts.py
  • src/openclatura/chains.py
  • src/openclatura/charge_specs.py
  • src/openclatura/component_modifiers.py
  • src/openclatura/component_namer.py
  • src/openclatura/data/parser_grammar_snapshot.json
  • src/openclatura/engine.py
  • src/openclatura/fused_ion_templates.py
  • src/openclatura/heteroatom_subgraphs.py
  • src/openclatura/ionic_naming.py
  • src/openclatura/locants.py
  • src/openclatura/molecule.py
  • src/openclatura/name_assembly.py
  • src/openclatura/name_operations.py
  • src/openclatura/namer.py
  • src/openclatura/naming_context.py
  • src/openclatura/oxoacid_templates.py
  • src/openclatura/perception.py
  • src/openclatura/retained_fused_production.py
  • src/openclatura/rules/elision.py
  • src/openclatura/small_ring_stereo.py
  • src/openclatura/special_cases.py
  • src/openclatura/tests/test_analysis.py
  • src/openclatura/von_baeyer.py
  • tests/integration/test_describer.py
  • website/.gitignore
  • website/api/index.py
  • website/public/index.html
  • website/public/ketcher/asset-manifest.json
  • website/public/ketcher/index.html
  • website/public/ketcher/manifest.json
  • website/public/ketcher/robots.txt
  • website/public/ketcher/serve.json
  • website/public/ketcher/static/css/622.6d0c2d59.chunk.css
  • website/public/ketcher/static/css/closable.9cca8bc6.css
  • website/public/ketcher/static/css/duo.0ae0f354.css
  • website/public/ketcher/static/css/main.9cca8bc6.css
  • website/public/ketcher/static/css/popup.9cca8bc6.css
  • website/public/ketcher/static/js/157.7de4e426.chunk.js
  • website/public/ketcher/static/js/535.c5fc3b49.chunk.js
  • website/public/ketcher/static/js/535.c5fc3b49.chunk.js.LICENSE.txt
  • website/public/ketcher/static/js/622.ed91acd0.chunk.js
  • website/public/ketcher/static/js/main.cb80d824.js
  • website/public/ketcher/static/js/main.cb80d824.js.LICENSE.txt
  • website/requirements.txt
  • website/scripts/cache_size.py
  • website/vercel.json
💤 Files with no reviewable changes (9)
  • src/openclatura/charge_specs.py
  • src/openclatura/oxoacid_templates.py
  • src/openclatura/rules/elision.py
  • src/openclatura/name_operations.py
  • src/openclatura/von_baeyer.py
  • src/openclatura/ionic_naming.py
  • src/openclatura/naming_context.py
  • src/openclatura/fused_ion_templates.py
  • src/openclatura/special_cases.py

Comment on lines +15 to +28
# Token spans map name characters back to atoms for the trace/describe output only; the
# final name string never depends on them. The engine disables their construction on the
# pure-name path (no trace requested), which skips ~10% of naming work.
_BUILD_TOKEN_SPANS = True


def set_token_span_building(enabled: bool) -> bool:
"""Toggle token-span construction; returns the previous setting for restoration."""

global _BUILD_TOKEN_SPANS
previous = _BUILD_TOKEN_SPANS
_BUILD_TOKEN_SPANS = enabled
return previous

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "ThreadPoolExecutor|threading\.Thread|run_in_threadpool|async def|WSGI|asgi" website/api/index.py

Repository: lamalab-org/openclatura

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Changed files/stat:"
git diff --stat || true

echo
echo "Relevant name_assembly lines:"
sed -n '1,90p' src/openclatura/name_assembly.py
sed -n '300,400p' src/openclatura/name_assembly.py

echo
echo "References to set_token_span_building / BUILD_TOKEN_SPANS:"
rg -n "set_token_span_building|_BUILD_TOKEN_SPANS|token_spans|need_analysis|trace" src website tests || true

echo
echo "Candidate API/entrypoint files:"
fd -i 'index\.py|.*api.*|.*main.*|.*app.*|.*service.*' . | sed -n '1,120p'

Repository: lamalab-org/openclatura

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked files matching relevant names:"
git ls-files | rg -n 'src/openclatura/(name_assembly|engine)\.py$|website/api/index\.py$|tests/.*/api|api' || true

echo
echo "name_assembly outline:"
ast-grep outline src/openclatura/name_assembly.py 2>/dev/null | sed -n '1,160p' || true

echo
echo "name_assembly relevant sections:"
sed -n '1,80p' src/openclatura/name_assembly.py
sed -n '318,365p' src/openclatura/name_assembly.py
sed -n '376,388p' src/openclatura/name_assembly.py

echo
echo "engine.py run/save-restore references:"
rg -n "set_token_span_building|previous_span_building|NamingEngine|need_analysis|analysis|trace" src/openclatura src website tests -g '*.py' | sed -n '1,220p'

Repository: lamalab-org/openclatura

Length of output: 33323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "website/api/index.py outline:"
ast-grep outline website/api/index.py 2>/dev/null | sed -n '1,220p' || true

echo
echo "website/api/index.py:"
sed -n '1,260p' website/api/index.py

echo
echo "engine.py NamingEngine.run sections:"
sed -n '138,240p' src/openclatura/engine.py

echo
echo "Deterministic concurrency simulation of save/restore global toggle."
python3 - <<'PY'
# This models interleaved reads/writes of the module-level global used by
# NamingEngine.run(): the first request may not build spans because another
# request's toggle/cleanup writes over the global in between.
BUILD_TOKEN_SPANS_GLOBAL = True
events = []

def reader():
    return BUILD_TOKEN_SPANS_GLOBAL

def writer(x):
    global BUILD_TOKEN_SPANS_GLOBAL
    BUILD_TOKEN_SPANS_GLOBAL = x
    events.append(("global_write", x))

# Request A saves True and enables token spans.
writer(True)
previous_a = reader()
writer(True)

# Request B interrupts, saves True and disables token spans.
previous_b = reader()
writer(False)
# B needs span building here, but the global is now False.
events.append(("request_b_build_now", reader()))

# Request A finishes/cleanup; its restored value is the value it saved earlier,
# not another thread's save.
writer(previous_a)
events.append(("request_a_restore", reader()))
# Request B's cleanup also writes saved value onto the global.
writer(previous_b)
events.append(("request_b_restore", reader()))

for e in events:
    print(f"{e[1]!r}")
PY

Repository: lamalab-org/openclatura

Length of output: 16545


Make the span-building flag request-local in shared processes.

website/api/index.py already serializes /api/batch via _OPSIN_LOCK before calling NamingEngine(); batch(...), so trace-analysis requests can share a Python process and call NamingEngine.run() concurrently. The bare _BUILD_TOKEN_SPANS global can be overwritten mid-flight by a concurrent request, so one request may build token spans in non-request mode or omit an included analysis trace. Move the toggle into a thread/task-local request context and have the token-span path read that context instead of the module global.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openclatura/name_assembly.py` around lines 15 - 28, The global
_BUILD_TOKEN_SPANS toggle in set_token_span_building is unsafe for concurrent
NamingEngine.run requests. Replace it with thread/task-local request context,
ensure set_token_span_building preserves and returns the caller’s prior context
value, and update the token-span construction path to read that context so each
request independently controls span building.

Comment thread src/openclatura/namer.py
Comment on lines +1238 to +1245
if n_substituents:
sub_exclude = set(exclude_atoms) | {n_idx} | acid_atoms
sub_names = [str(name_subgraph(mol, s, sub_exclude, upstream_atom=n_idx)) for s in n_substituents]
if not all(sub_names):
return None
n_prefix = _format_n_locant_prefix(sub_names)
amido = f"{n_prefix}-{amido}" if amido[0].isdigit() else f"{n_prefix}{amido}"
return f"({amido})" if is_complex_prefix(amido) else amido

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Missing hyphen when the acid fragment's name starts with a parenthesized stereo descriptor.

The hyphen-insertion check only tests amido[0].isdigit(). When the acyl acid component's own name begins with a bracketed stereo descriptor (e.g. "(2S)-2-aminopropanoic acid"amido = "(2S)-2-aminopropanamido"), this branch is skipped and the N-locant prefix is concatenated directly, producing a malformed name like "N-methyl(2S)-2-aminopropanamido" instead of "N-methyl-(2S)-2-aminopropanamido".

🐛 Proposed fix
-        amido = f"{n_prefix}-{amido}" if amido[0].isdigit() else f"{n_prefix}{amido}"
+        amido = f"{n_prefix}-{amido}" if amido[0] in "(0123456789" else f"{n_prefix}{amido}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if n_substituents:
sub_exclude = set(exclude_atoms) | {n_idx} | acid_atoms
sub_names = [str(name_subgraph(mol, s, sub_exclude, upstream_atom=n_idx)) for s in n_substituents]
if not all(sub_names):
return None
n_prefix = _format_n_locant_prefix(sub_names)
amido = f"{n_prefix}-{amido}" if amido[0].isdigit() else f"{n_prefix}{amido}"
return f"({amido})" if is_complex_prefix(amido) else amido
if n_substituents:
sub_exclude = set(exclude_atoms) | {n_idx} | acid_atoms
sub_names = [str(name_subgraph(mol, s, sub_exclude, upstream_atom=n_idx)) for s in n_substituents]
if not all(sub_names):
return None
n_prefix = _format_n_locant_prefix(sub_names)
amido = f"{n_prefix}-{amido}" if amido[0] in "(0123456789" else f"{n_prefix}{amido}"
return f"({amido})" if is_complex_prefix(amido) else amido
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openclatura/namer.py` around lines 1238 - 1245, Update the amido prefix
concatenation in the n_substituents branch to insert a hyphen when amido begins
with either a digit or a parenthesized stereo descriptor, while preserving the
existing no-hyphen behavior for other prefixes. Use the existing amido and
_format_n_locant_prefix logic without changing downstream complex-prefix
formatting.

Comment thread website/api/index.py
Comment on lines +58 to +90
def _preload_xlibs() -> None:
if not _XLIBS_TARBALL.exists():
return
if not (_XLIBS_DIR / _XLIBS_ORDER[-1]).exists():
staging = Path(tempfile.mkdtemp(dir=tempfile.gettempdir()))
with tarfile.open(_XLIBS_TARBALL) as tar:
tar.extractall(staging)
try:
staging.rename(_XLIBS_DIR)
except OSError: # concurrent cold start already extracted it
shutil.rmtree(staging, ignore_errors=True)
for soname in _XLIBS_ORDER:
try:
ctypes.CDLL(str(_XLIBS_DIR / soname), mode=ctypes.RTLD_GLOBAL)
except OSError:
pass # already provided by the system, or depiction stays off


def _ensure_java() -> None:
if shutil.which("java"):
return
if not _JRE_TARBALL.exists():
return
if not (_JRE_DIR / "bin" / "java").exists():
staging = Path(tempfile.mkdtemp(dir=tempfile.gettempdir()))
with tarfile.open(_JRE_TARBALL) as tar:
tar.extractall(staging)
try:
staging.rename(_JRE_DIR)
except OSError: # concurrent cold start already extracted it
shutil.rmtree(staging, ignore_errors=True)
os.environ["PATH"] = f"{_JRE_DIR / 'bin'}{os.pathsep}{os.environ.get('PATH', '')}"
os.environ.setdefault("JAVA_HOME", str(_JRE_DIR))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Harden tar.extractall() against path traversal (Zip Slip).

Both _preload_xlibs and _ensure_java call tar.extractall(staging) with no member-path validation. Actual exploitability is low here since jre.tar.gz/xlibs.tar.gz are bundled by your own build (not attacker-supplied), but it's a one-line hardening fix.

🔒 Proposed fix
     with tarfile.open(_XLIBS_TARBALL) as tar:
-        tar.extractall(staging)
+        tar.extractall(staging, filter="data")

and similarly for the JRE extraction.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 63-63: Calling extractall() on a zipfile.ZipFile or tarfile archive without validating member paths lets a crafted entry (e.g. "../../etc/passwd") write outside the destination directory (Zip Slip). Validate each member resolves inside the target directory, or pass a safe filter (tarfile: filter="data" / tarfile.data_filter).
Context: tar.extractall(staging)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(archive-extractall-path-traversal-python)


[error] 83-83: Calling extractall() on a zipfile.ZipFile or tarfile archive without validating member paths lets a crafted entry (e.g. "../../etc/passwd") write outside the destination directory (Zip Slip). Validate each member resolves inside the target directory, or pass a safe filter (tarfile: filter="data" / tarfile.data_filter).
Context: tar.extractall(staging)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(archive-extractall-path-traversal-python)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/api/index.py` around lines 58 - 90, Harden both tar extraction sites
in _preload_xlibs and _ensure_java by validating archive member paths before
extraction, rejecting any entry that resolves outside the staging directory.
Apply the same safe-extraction approach to both tar.extractall calls while
preserving their existing staging and cleanup behavior.

Source: Linters/SAST tools

Comment thread website/api/index.py
Comment on lines +249 to +403
# py2opsin writes a fixed-name temp file in the CWD, so concurrent
# requests in one warm process corrupt each other's OPSIN round-trip.
# Serialize the verify path (and retry once for residual flakes).
_OPSIN_LOCK = threading.Lock()


class _OpsinDaemon:
"""Long-lived OPSIN CLI process, one per instance.

Spawning a JVM per verification costs ~1.5 s; OPSIN's CLI streams
names line-by-line (empty output line = unparseable), so one warm
JVM answers in milliseconds. Access is serialized by _OPSIN_LOCK.
On any protocol hiccup the process is killed and the caller falls
back to the one-shot py2opsin path.
"""

def __init__(self) -> None:
self._proc: subprocess.Popen | None = None

@staticmethod
def _jar_path() -> str | None:
try:
import py2opsin
except ImportError:
return None
jars = sorted(Path(py2opsin.__file__).parent.glob("opsin-cli-*.jar"))
return str(jars[-1]) if jars else None

def _ensure(self) -> bool:
if self._proc is not None and self._proc.poll() is None:
return True
jar = self._jar_path()
if jar is None or not shutil.which("java"):
return False
self._proc = subprocess.Popen(
["java", "-jar", jar, "-osmi"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
return True

def parse(self, name: str, timeout: float = 15.0) -> str | None:
"""SMILES for ``name``, "" if OPSIN can't parse it, None if the
daemon is unavailable (caller should fall back)."""
if "\n" in name or "\r" in name or not self._ensure():
return None
proc = self._proc
try:
proc.stdin.write(name + "\n")
proc.stdin.flush()
ready, _, _ = select.select([proc.stdout], [], [], timeout)
if not ready:
raise TimeoutError(f"OPSIN gave no answer within {timeout}s")
line = proc.stdout.readline()
if line == "":
raise EOFError("OPSIN process closed stdout")
return line.rstrip("\n")
except Exception:
try:
proc.kill()
except Exception:
pass
self._proc = None
return None


_OPSIN_DAEMON = _OpsinDaemon()


def _opsin_check_via_daemon(name: str, smiles: str):
"""Mirror openclatura.opsin_verify.verify_with_opsin, but decode the
name through the persistent JVM. Returns None to request fallback."""
from openclatura.opsin_verify import OpsinCheck
from openclatura.resonance_compare import canonical_smiles, equivalent_smiles
from openclatura.utils import standardize_mol

if not name:
return OpsinCheck(status="name_empty", name=name)
decoded = _OPSIN_DAEMON.parse(name)
if decoded is None:
return None
canonical_original = standardize_mol(smiles)
if decoded == "":
return OpsinCheck(status="name_unparseable", name=name, canonical_original=canonical_original)
canonical_roundtrip = canonical_smiles(decoded)
if canonical_original is None or canonical_roundtrip is None:
return OpsinCheck(
status="error",
name=name,
canonical_original=canonical_original,
opsin_smiles=decoded,
canonical_roundtrip=canonical_roundtrip,
error_message="Failed to standardize SMILES for comparison.",
)
return OpsinCheck(
status="matched" if equivalent_smiles(smiles, decoded) else "mismatched",
name=name,
canonical_original=canonical_original,
opsin_smiles=decoded,
canonical_roundtrip=canonical_roundtrip,
)


class NameRequest(BaseModel):
smiles: str
include_trace: bool = False
verify_opsin: bool = False
token_debug: bool = False


def _name_cacheable(payload: dict, verify: bool) -> bool:
if not payload.get("ok"):
return False
if verify:
status = (payload.get("opsin_check") or {}).get("status")
return status in ("matched", "mismatched", "name_unparseable")
return True


@app.post("/api/name")
def name_endpoint(req: NameRequest) -> dict:
"""Shadows the mounted app's /name: adds result caching and makes
OPSIN verification safe under in-process request concurrency."""
flags = f"t{int(req.include_trace)}v{int(req.verify_opsin)}d{int(req.token_debug)}"
key = _cache_key("name", req.smiles, flags)
cached = _cache_get(key)
if cached is not None:
return cached
if not req.verify_opsin:
result = name_one(req.smiles, include_trace=req.include_trace, token_debug=req.token_debug)
else:
with _OPSIN_LOCK:
# include_trace=True mirrors the engine's verify branch, which
# always analyzes; to_dict() below trims it when not requested.
result = name_one(req.smiles, include_trace=True, token_debug=req.token_debug)
check = None if result.error else _opsin_check_via_daemon(result.name, req.smiles)
if check is not None:
result = dataclasses.replace(result, opsin_check=check)
else:
for attempt in (1, 2):
result = name_one(
req.smiles,
include_trace=req.include_trace,
verify_opsin=True,
token_debug=req.token_debug,
)
if result.opsin_check is None or result.opsin_check.status != "error":
break
payload = result.to_dict(include_trace=req.include_trace)
if _name_cacheable(payload, req.verify_opsin):
_cache_set(key, payload)
return payload

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the mounted app's /batch route performs OPSIN verification.
fd -e py . | xargs rg -nP -C3 '\bdef\s+batch\b|/batch|verify_opsin' 2>/dev/null

Repository: lamalab-org/openclatura

Length of output: 22071


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate opsin verify =="
fd -e py -a . | grep 'opsin_verify\.py$' || true

echo "== inspect source snippets =="
sed -n '1,220p' src/openclutura/opsin_verify.py 2>/dev/null || sed -n '1,260p' src/openclatura/opsin_verify.py 2>/dev/null || true

echo "== inspect engine one-shot verification line =="
sed -n '220,245p' src/openclatura/engine.py

echo "== inspect name_many/process pool =="
sed -n '245,285p' src/openclatura/engine.py
sed -n '423,445p' src/openclatura/engine.py

Repository: lamalab-org/openclatura

Length of output: 8921


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== imports of process pool and lock in api =="
sed -n '1,90p' website/api/index.py
sed -n '335,408p' website/api/index.py

echo "== exported web app and mounting =="
sed -n '1,130p' src/openclatura/web/app.py

echo "== read-only behavioral model of parallel py2opsin temp-file race =="
python3 - <<'PY'
import ast, re, sys
for path in ("src/openclatura/opsin_verify.py", "src/openclatura/engine.py"):
    text = open(path, "r", encoding="utf-8").read()
    tree = ast.parse(text, filename=path)
    names = []
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            # call py2opsin.py2opsin?
            cands = []
            for sub in ast.walk(node):
                if isinstance(sub, ast.Call):
                    call = getattr(sub.func, "id", "") if isinstance(sub.func, ast.Name) else None
                    if call == "py2opsin":
                        cands.append(sub)
            if cands:
                names.append(node.name)
    print(path, names, "contains call to py2opsin.py2opsin:", any("py2opsin" in text for _ in cands))

print("processes default:", bool(re.search(r"processes: *int *=[^=\n]*= *(1)", open("src/openclatura/engine.py").read())))
print("parallel uses ProcessPoolExecutor:", bool(re.search(r"ProcessPoolExecutor", open("src/openclatura/engine.py").read())))
PY

Repository: lamalab-org/openclatura

Length of output: 9451


Make OPSIN verification safe for parallel /api/batch.

name_many(..., processes != 1) runs verify_with_opsin(...) in worker processes via ProcessPoolExecutor, so enabled OPSIN verification can still invoke py2opsin concurrently. Either serialize OPSIN within verify_with_opsin/a shared daemon, or disable/offload OPSIN verification when batch runs with multiple processes.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 282-289: Command coming from incoming request
Context: subprocess.Popen(
["java", "-jar", jar, "-osmi"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/api/index.py` around lines 249 - 403, Extend the OPSIN concurrency
protection beyond name_endpoint: ensure name_many batch execution with processes
!= 1 cannot invoke verify_with_opsin/py2opsin concurrently across worker
processes. Prefer routing verification through a shared serialized daemon, or
disable/offload verification for that multi-process path while preserving
existing behavior for single-process and non-batch requests.

Comment thread website/api/index.py
Comment on lines +406 to +419
class DescribeRequest(BaseModel):
smiles: str


@app.post("/api/describe")
def describe_endpoint(req: DescribeRequest) -> dict:
"""Shadows the mounted app's /describe to add result caching."""
key = _cache_key("desc", req.smiles)
cached = _cache_get(key)
if cached is not None:
return cached
payload = describe(req.smiles).to_dict()
_cache_set(key, payload)
return payload

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

describe_endpoint has no error handling for unparsable SMILES.

Unlike depict (which explicitly returns {"ok": False, "error": ...} for bad SMILES), describe_endpoint calls describe(req.smiles) directly. If describe() raises rather than returning a structured error for invalid input, callers get a generic 500 instead of a consistent API error shape.

🛡️ Proposed defensive fix
 `@app.post`("/api/describe")
 def describe_endpoint(req: DescribeRequest) -> dict:
     """Shadows the mounted app's /describe to add result caching."""
     key = _cache_key("desc", req.smiles)
     cached = _cache_get(key)
     if cached is not None:
         return cached
-    payload = describe(req.smiles).to_dict()
+    try:
+        payload = describe(req.smiles).to_dict()
+    except Exception as exc:
+        return {"ok": False, "error": str(exc)}
     _cache_set(key, payload)
     return payload
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class DescribeRequest(BaseModel):
smiles: str
@app.post("/api/describe")
def describe_endpoint(req: DescribeRequest) -> dict:
"""Shadows the mounted app's /describe to add result caching."""
key = _cache_key("desc", req.smiles)
cached = _cache_get(key)
if cached is not None:
return cached
payload = describe(req.smiles).to_dict()
_cache_set(key, payload)
return payload
class DescribeRequest(BaseModel):
smiles: str
`@app.post`("/api/describe")
def describe_endpoint(req: DescribeRequest) -> dict:
"""Shadows the mounted app's /describe to add result caching."""
key = _cache_key("desc", req.smiles)
cached = _cache_get(key)
if cached is not None:
return cached
try:
payload = describe(req.smiles).to_dict()
except Exception as exc:
return {"ok": False, "error": str(exc)}
_cache_set(key, payload)
return payload
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/api/index.py` around lines 406 - 419, Update describe_endpoint to
handle exceptions raised by describe(req.smiles), returning the same {"ok":
False, "error": ...} structured response used by depict for unparsable SMILES
instead of propagating a generic 500. Preserve the existing cache lookup and
successful-response caching behavior.

Comment thread website/public/index.html
Comment on lines +368 to +372
ketcher.editor.subscribe("change", function () {
clearTimeout(debounceTimer);
clearTimeout(failureTimer); // still drawing: a pending failure is moot
debounceTimer = setTimeout(nameCurrent, EDIT_DEBOUNCE_MS);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Version asynchronous work by editor revision. Increment a revision on every Ketcher change and require asynchronous completions to match it before mutating the UI.

  • website/public/index.html#L368-L372: increment a shared editor revision before scheduling naming.
  • website/public/index.html#L403-L418: capture and validate that revision around getSmiles() before calling requestName().
  • website/public/index.html#L534-L599: capture and validate the revision before rendering explanation or depiction results.
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 370-370: React's useState should not be directly called
Context: setTimeout(nameCurrent, EDIT_DEBOUNCE_MS)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)


[warning] 370-370: Avoid using the initial state variable in setState
Context: setTimeout(nameCurrent, EDIT_DEBOUNCE_MS)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

📍 Affects 1 file
  • website/public/index.html#L368-L372 (this comment)
  • website/public/index.html#L403-L418
  • website/public/index.html#L534-L599
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/public/index.html` around lines 368 - 372, Version asynchronous
editor work by a shared revision counter. In website/public/index.html:368-372,
increment the revision on every Ketcher change before scheduling naming; in
website/public/index.html:403-418, capture the revision and validate it after
getSmiles() before calling requestName(); and in
website/public/index.html:534-599, capture and validate the revision before
rendering explanation or depiction results. Ensure stale completions cannot
mutate the UI.

Comment thread website/requirements.txt
Comment on lines +1 to +2
openclatura[web]==0.1.4
py2opsin>=1.2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm which installed package pulls in the vulnerable pillow/h11 versions.
pip download openclatura[web]==0.1.4 --no-deps -d /tmp/ocw 2>&1 | tail -5
pip show openclatura 2>/dev/null

Repository: lamalab-org/openclatura

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== requirements files =="
fd -a 'requirements.*txt$|pyproject.toml$|setup.py$|setup.cfg$' . | sed 's#^\./##'

echo
echo "== website/requirements.txt =="
cat -n website/requirements.txt

echo
echo "== package metadata snippets =="
python3 - <<'PY'
import zipfile
from pathlib import Path

p = next(Path("/tmp/ocw").glob("openclatura-0.1.4*.whl"))
with zipfile.ZipFile(p) as z:
    names = [n for n in z.namelist() if n.endswith(('.dist-info/METADATA','.egg-info/SOURCES.txt','.egg-info/requires.txt')) or n.endswith(('.egg-info/top_level.txt','.egg-info/entry_points.txt'))]
    for name in sorted(names):
        print(f"\n--- {name} ---")
        try:
            txt = z.read(name).decode('utf-8', errors='replace')
        except Exception as e:
            print(type(e).__name__, e)
            continue
        for line in txt.splitlines()[:80]:
            if any(s in line.lower() for s in ['pillow','h11','requires','requires-python','project-url','home-page']):
                print(line)
PY

echo
echo "== dependency resolver snapshot for transitive deps =="
python3 - <<'PY'
import subprocess, sys, tempfile, os, pathlib, re
req = pathlib.Path("website/requirements.txt").read_text().strip().splitlines()
print(req)
with tempfile.TemporaryDirectory() as d:
    r = pathlib.Path(d) / "requirements.txt"
    r.write_text("\n".join(req))
    try:
        out = subprocess.check_output([sys.executable, "-m", "pip", "install", "--dry-run", "--ignore-installed", "--no-index", "--find-links=/tmp/ocw", "-r", str(r)], stderr=subprocess.STDOUT, text=True, timeout=120)
    except subprocess.CalledProcessError as e:
        print("STDOUT:")
        print(e.stdout.splitlines()[-40:] if e.stdout else "")
        print("STDERR:")
        print(e.stderr.splitlines()[-80:] if e.stderr else "")
        raise SystemExit(0)
    lines = out.splitlines()
    for i,l in enumerate(lines):
        if l.lower().startswith("would install"):
            for j in range(i, min(i+200, len(lines))):
                print(lines[j])
            break
PY

echo
echo "== installed pillow/h11 in sandbox? =="
pip show pillow h11 2>/dev/null || true

echo
echo "== search for local config constraints or OSV config =="
rg -n "h11|pillow|openclatura|py2opsin|osv|scanning|requirements" . --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' 2>/dev/null | head -200

Repository: lamalab-org/openclatura

Length of output: 40986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect web dependency metadata =="
python3 - <<'PY'
import zipfile
from pathlib import Path

for whl in sorted(Path("/tmp/ocw").glob("*.whl")):
    if "openclatura" not in whl.name:
        continue
    print(f"\n=== {whl.name} ===")
    with zipfile.ZipFile(whl) as z:
        candidates = [n for n in z.namelist() if n.endswith(".dist-info/METADATA") or n.endswith(".egg-info/requires.txt")]
        for cand in candidates:
            txt = z.read(cand).decode("utf-8", errors="replace")
            print(f"\n-- METADATA/requires at {cand} --")
            for line in txt.splitlines():
                if "web" in line.lower() or "pillow" in line.lower() or "h11" in line.lower() or line.startswith("Requires-Dist:") or line.startswith("Requires-Python:"):
                    print(line)
PY

Repository: lamalab-org/openclatura

Length of output: 1380


🌐 Web query:

uvicorn standard extra pillow h11 dependencies

💡 Result:

uvicorn[standard] installs: uvloop, httptools, websockets, watchfiles, python-dotenv, and PyYAML. The base uvicorn install also includes h11 and click (and typing_extensions on Python 3.10/earlier). Pillow is not part of Uvicorn’s dependencies. (uvicorn.dev)

Citations:


🌐 Web query:

OSV Pillow 9.5.0 CVE RCE DoS buffer overflow

💡 Result:

Pillow 9.5.0 is affected by CVE-2024-28219, a buffer overflow in _imagingcms.c caused by strcpy being used instead of strncpy. OSV lists 9.5.0 among affected versions, and the issue is fixed in Pillow 10.3.0. It can lead to denial of service and potentially arbitrary code execution (RCE). [1][2][3]

If you want, I can also give you the exact OSV entry and a one-line remediation for package manifests.


🌐 Web query:

OSV H11 0.9.0 malformed chunked encoding CVE

💡 Result:

The vulnerability regarding malformed chunked encoding in the h11 Python HTTP/1.1 library is tracked as CVE-2025-43859 [1][2]. Details: The vulnerability arises from a leniency in h11's parsing of line terminators in chunked-coding message bodies [3][1]. Specifically, prior to version 0.16.0, h11 did not strictly validate that the trailing \r\n bytes in chunked-encoding chunks were correct [3][4]. If malformed bytes were provided, h11 would accept them instead of rejecting the body as malformed [1][4]. This behavior can lead to HTTP request smuggling vulnerabilities if an h11-based application is placed behind an intermediary (such as a proxy or reverse-proxy) that parses the same chunked-encoding stream differently [3][1]. If both systems accept the same stream but interpret it as different request structures, an attacker may be able to smuggle requests to the backend [3][4]. Mitigation: This issue was fixed in h11 version 0.16.0 [1][2]. Upgrading to version 0.16.0 or later is the recommended way to resolve the vulnerability [2][5]. Because the vulnerability requires a combination of a buggy h11 implementation and a buggy intermediary, fixing either component is sufficient to mitigate the risk [2][6]. The vulnerability is categorized under CWE-444: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling') [1][6].

Citations:


🌐 Web query:

fastapi uvicorn standard dependencies pillow h11

💡 Result:

If you’re asking what these mean together:

  • FastAPI: web framework for APIs. Its fastapi[standard] install pulls in recommended extras, including Uvicorn. (pypi.org)
  • Uvicorn: ASGI server. uvicorn installs minimal deps including h11 and click; uvicorn[standard] adds extra performance/recommended packages like uvloop. (uvicorn.org)
  • h11: pure-Python HTTP/1.1 implementation used by Uvicorn. (uvicorn.org)
  • Pillow: image-processing library; it’s unrelated to FastAPI/Uvicorn and is not part of their standard dependencies. (pypi.org)

So, in short: FastAPI → Uvicorn → h11 is the web stack; Pillow is separate. (pypi.org)

Citations:


🌐 Web query:

py2opsin dependencies Pillow

💡 Result:

py2opsin has zero Python dependencies, so Pillow is not required for it. The package bundles OPSIN itself. (pypi.org)

Citations:


🌐 Web query:

fastapi pydantic pillow dependency

💡 Result:

FastAPI depends on Pydantic and Starlette; Pillow is not a FastAPI/Pydantic dependency. If you need image handling, install Pillow separately (pip install pillow). (fastapi.tiangolo.com)

If you want, I can also give you the exact pip/uv install line for a FastAPI + Pydantic + Pillow project.

Citations:


Pin the vulnerable h11 transitive dependency.

openclatura[web] brings in uvicorn, whose base dependency tree includes h11. h11 versions prior to 0.16.0 are affected by CVE-2025-43859; add an explicit constraint here such as h11>=0.16.0. pillow/PyYAML are not dependencies of this requirements file’s direct package specs, so this file does not need a Pillow override.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[CRITICAL] 1-1: h11 0.9.0: h11 accepts some malformed Chunked-Encoding bodies

(PYSEC-2026-348)


[CRITICAL] 1-1: h11 0.9.0: h11 accepts some malformed Chunked-Encoding bodies

(GHSA-vqfr-h8mv-ghfj)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2023-175)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2023-227)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-165)


[CRITICAL] 1-1: pillow 9.5.0: Pillow buffer overflow vulnerability

(PYSEC-2026-1793)


[CRITICAL] 1-1: pillow 9.5.0: libwebp: OOB write in BuildHuffmanTable

(PYSEC-2026-1794)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-2253)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-2254)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-2255)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-2256)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-2257)


[CRITICAL] 1-1: pillow 9.5.0: Pillow has a PDF Parsing Trailer Infinite Loop (DoS)

(PYSEC-2026-2874)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-3451)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-3453)


[CRITICAL] 1-1: pillow 9.5.0: undefined

(PYSEC-2026-3454)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files)

(PYSEC-2026-3493)


[CRITICAL] 1-1: pillow 9.5.0: Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images

(PYSEC-2026-3494)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Decompression Bomb DoS via PdfParser.PdfStream.decode()

(PYSEC-2026-3495)


[CRITICAL] 1-1: pillow 9.5.0: Pillow JPEG2000 tiled decode retains a growing scratch buffer and can be used for denial of service

(PYSEC-2026-3496)


[CRITICAL] 1-1: pillow 9.5.0: Arbitrary Code Execution in Pillow

(PYSEC-2026-457)


[CRITICAL] 1-1: pillow 9.5.0: Arbitrary Code Execution in Pillow

(GHSA-3f63-hfp8-52jq)


[CRITICAL] 1-1: pillow 9.5.0: Pillow buffer overflow vulnerability

(GHSA-44wm-f244-xhp3)


[CRITICAL] 1-1: pillow 9.5.0: Pillow BdfFontFile: Image.new() called without _decompression_bomb_check() — bomb protection bypass via font loading

(GHSA-45hq-cxwh-f6vc)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: WindowsViewer.get_command() OS command injection via unescaped shell path

(GHSA-4x4j-2g7c-83w6)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: FontFile.compile(): Image.new() called without _decompression_bomb_check()

(GHSA-5x94-69rx-g8h2)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Out-of-bounds read via attacker-controlled row stride on Pillow's mmap path (McIdas AREA files)

(GHSA-62p4-gmf7-7g93)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Heap out-of-bounds write Image.paste() / Image.crop() via signed coordinate overflow

(GHSA-6r8x-57c9-28j4)


[CRITICAL] 1-1: pillow 9.5.0: Pillow Denial of Service vulnerability

(GHSA-8ghj-p4vj-mr35)


[CRITICAL] 1-1: pillow 9.5.0: Pillow PcfFontFile._load_bitmaps(): Image.frombytes() called without _decompression_bomb_check() — bomb protection bypass via PCF font loading

(GHSA-8v84-f9pq-wr9x)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Controlled heap out-of-bounds write in Pillow ImageCmsTransform.apply() via output mode mismatch

(GHSA-9hw9-ch79-4vh6)


[CRITICAL] 1-1: pillow 9.5.0: Pillow TGA RLE encoder can serialize up to ~57 KB of adjacent heap data into generated images

(GHSA-fj7v-r99m-22gq)


[CRITICAL] 1-1: pillow 9.5.0: libwebp: OOB write in BuildHuffmanTable

(GHSA-j7hp-h8jx-5ppr)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Decompression Bomb DoS via PdfParser.PdfStream.decode()

(GHSA-jjj6-mw9f-p565)


[CRITICAL] 1-1: pillow 9.5.0: Pillow GdImageFile._open(): image dimensions accepted without _decompression_bomb_check()

(GHSA-phj9-mv4w-65pm)


[CRITICAL] 1-1: pillow 9.5.0: Pillow has a PDF Parsing Trailer Infinite Loop (DoS)

(GHSA-r73j-pqj5-w3x7)


[CRITICAL] 1-1: pillow 9.5.0: Pillow JPEG2000 tiled decode retains a growing scratch buffer and can be used for denial of service

(GHSA-vjc4-5qp5-m44j)


[CRITICAL] 1-1: pillow 9.5.0: Pillow has an integer overflow when processing fonts

(GHSA-wjx4-4jcj-g98j)


[CRITICAL] 1-1: pillow 9.5.0: Pillow: Heap out-of-bounds write in ImageFilter.RankFilter via integer overflow in ImagingExpand

(GHSA-xj96-63gp-2gmr)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/requirements.txt` around lines 1 - 2, Add an explicit h11>=0.16.0
constraint to website/requirements.txt alongside the existing package
specifications, while leaving the openclatura and py2opsin entries unchanged.

Source: Linters/SAST tools

@r-fedorov
r-fedorov merged commit 78a0c13 into main Jul 24, 2026
22 checks passed
@r-fedorov
r-fedorov deleted the tests branch July 24, 2026 11:05
@r-fedorov
r-fedorov restored the tests branch July 24, 2026 11:05
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