Skip to content

VEX product detection fails on Poetry 2 projects that declare dynamic = ["version"] with the version in [tool.poetry], so scan --vex exits 1 after rewriting poetry.lock #1527

Closed

Description

[agent] Found by the scheduled Poetry bug-hunt routine (ledger #311).

Summary

Poetry 2 documents a PEP 621 layout where [project] has name and dynamic = ["version"], and the version is in [tool.poetry] version (see https://python-poetry.org/docs/pyproject/#version). poetry check, poetry version and poetry build all accept it (demo 0.3.0, demo-0.3.0-py3-none-any.whl).

vex/product.rs::parse_pyproject reads name and version from one section only. It tries [project], which has no version, and then [tool.poetry], which has no name. Both return None, so no product is detected:

  • socket-patch vex -O out.json exits 2 with product_undetected ("pyproject.toml was found but has no usable name and version").
  • socket-patch scan --mode hosted --vex out.json exits 1 with product_undetected, after it has already rewritten poetry.lock to the hosted pin. No VEX document is written.

This happens whenever the product falls through to the manifest probe, which means a checkout with no git origin remote (for example a source tarball or an exported tree). A git remote takes priority in detect_product.

This isn't the same as #642. That issue is about a missing [tool.pdm] fallback. Here both sections are read, but their keys are never combined.

Impact

  • Any Poetry 2 project that uses the documented dynamic-version layout gets no VEX unless the user passes --product by hand, and scan --vex turns a successful hosted rewrite into a failed command (exit 1) in CI.
  • The lock rewrite is still committed, so a retry without --vex has nothing left to do, and the failure looks like a broken scan.

Repro (Linux, Poetry 2.0.1 / 2.2.1 / 2.5.1, no git checkout)

A local mock of the patch API serves a hosted patch for pkg:pypi/six@1.16.0 (same routes as crates/socket-patch-cli/tests/in_process_redirect_poetry.rs). Any patched dependency would do.

mkdir demo-proj && cd demo-proj && mkdir demo && touch demo/__init__.py
cat > pyproject.toml <<'EOF'
[project]
name = "demo"
dynamic = ["version"]
requires-python = ">=3.9"
dependencies = ["six==1.16.0"]

[tool.poetry]
version = "0.3.0"

[build-system]
requires = ["poetry-core>=2.0.0"]
build-backend = "poetry.core.masonry.api"
EOF
poetry check          # All set!
poetry version        # demo 0.3.0
poetry lock
socket-patch scan --mode hosted --json --vex out.vex.json
#  exit 1, status "error", error.code "product_undetected"
#  poetry.lock now carries the hosted pin; out.vex.json not written
socket-patch vex -O v.json                               # exit 2, product_undetected
socket-patch vex -O v.json --product pkg:pypi/demo@0.3.0 # exit 0 (workaround)

Control: with version = "0.3.0" in [project] (no dynamic, no [tool.poetry]), scan --mode hosted --vex exits 0 and the product is pkg:pypi/demo@0.3.0.

Expected vs actual

  • Expected: vex --help documents auto-detection from "pyproject.toml: pkg:pypi/@". For a project Poetry itself builds as demo 0.3.0, the product should be pkg:pypi/demo@0.3.0: [project].name, plus the [tool.poetry].version that Poetry uses when version is listed in [project].dynamic. CLI_CONTRACT.md ("Fail-the-command") says scan --vex exits non-zero when the product is undetectable, but here it can be detected.
  • Actual: product_undetected. vex exits 2, and scan --vex exits 1 after it has written the lock.

OS × version

OS Poetry Result
Linux 2.0.1 fail (2/2)
Linux 2.2.1 fail
Linux 2.5.1 fail (2/2)
Linux 2.5.1, static [project].version (control) pass
macOS / Windows any untested. The code path doesn't depend on the OS (pure TOML text scan).

Poetry 1.x ignores [project], so the layout only applies to Poetry ≥ 2.0.

Suspect code

  • crates/socket-patch-core/src/vex/product.rs:157-165 (parse_pyproject): scan_toml_section(content, "project").or_else(|| scan_toml_section(content, "tool.poetry")) needs both keys from the same section. When [project] has a name and dynamic contains "version", it should take the version from [tool.poetry].
  • crates/socket-patch-core/src/vex/product.rs:368 (scan_toml_section) returns nothing unless both keys are present.

Main 6a06ddd. No probe runs: probe branches are still blocked for this routine (see ledger #311).

Activity

  1. added a commit that references this issue on Oct 14, 2026
  2. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triage: priority:p1 (Poetry, PyPI family). Not a duplicate: #642 is a different symptom (PDM 0.x [tool.pdm]), and no open or merged PR covers it. Confirmed on main a8da202: parse_pyproject (crates/socket-patch-core/src/vex/product.rs:157) calls scan_toml_section("project").or_else(|| scan_toml_section("tool.poetry")), and each call needs both keys from its own table.

    Shares root cause with #642: vex/product.rs::parse_pyproject takes name and version from one table only ([project], else [tool.poetry]), so it never combines [project].name with a dynamic version kept in [tool.poetry] and never reads the legacy [tool.pdm] table. Will be fixed together. The scan --vex write-before-product ordering both reports mention is tracked separately in #1064.


    Generated by Claude Code

  3. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (with #642; shared root cause: pyproject product detection reads name and version from a single table). Branch: agent/fix-pyproject-product-tables. Claim-ID: 2026-10-14T10:21:23Z-28fe92


    Generated by Claude Code

  4. mikolalysenko commented on Oct 14, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1529


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions