Repository navigation
Poetry venv discovery expands a {project-dir} placeholder Poetry doesn't have, so agent mode misses the env, patches the global interpreter, and VEX attests not_affected #608
Description
[agent] Found by the scheduled Poetry bug-hunt routine (ledger #311).
Summary
poetry_virtualenvs_path (crates/socket-patch-core/src/crawlers/python_crawler.rs:1003-1005) treats {project-dir} in virtualenvs.path as a placeholder for the project directory. Poetry has no such placeholder. Its Config.process() only substitutes keys that exist in its own config ({cache-dir}, {data-dir}, …), and project-dir isn't one of them:
- Poetry 1.8.5, 2.0.1 and 2.5.1 keep the text literally ("might be resolved later"), so the env is created under a directory literally named
{project-dir}inside the project:<proj>/{project-dir}/.envs/<name>-<hash>-pyX.Y. - Poetry 1.1.15 turns it into an empty string, so the env lands in
/.envs/<name>-<hash>-pyX.Y.
Checked in poetry/config/config.py (process / virtualenvs_path) for 1.1.15, 1.8.5, 2.0.1 and 2.5.1, and confirmed with poetry env info -p.
socket-patch looks in <proj>/.envs instead, which doesn't exist. Discovery then falls through ./.venv and ./venv to the documented global-interpreter fallback.
Impact
With virtualenvs.path = "{project-dir}/.envs" in poetry.toml, scan --mode agent does three things:
- It patches whatever global copy matches. In this case that was the apt-owned
/usr/lib/python3/dist-packages/six.py(python3-six), a system package outside the project. - It leaves Poetry's env unpatched:
poetry run python -c "import six"imports the upstream bytes. - It reports
success,applied: 1, exit 0.socket-patch vexthen writes anot_affected/inline_mitigations_already_existstatement forpkg:pypi/demo@0.1.0.
That is a VEX attestation for a patch the project's runtime doesn't have, plus an unrequested write into a distro-managed file. With no global copy present, the result is a silent package_not_installed skip with exit 0.
The trigger is a non-standard config value (Poetry doesn't document {project-dir}), but the code and its unit test (python_crawler.rs:3550-3556) encode it as supported. The fallout is the worst kind: a wrong-target write plus a false attestation.
Repro
Run as root (or anyone who can write the system six) on Linux, with Debian's python3-six 1.16.0 installed. Any global six==1.16.0 copy works too.
mkdir -p /tmp/pdr/proj/demo && cd /tmp/pdr/proj && touch demo/__init__.py
cat > pyproject.toml <<'EOF'
[tool.poetry]
name = "demo"
version = "0.1.0"
description = ""
authors = ["x <x@x.x>"]
[tool.poetry.dependencies]
python = "^3.10"
six = "1.16.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
EOF
printf '[virtualenvs]\npath = "{project-dir}/.envs"\n' > poetry.toml
poetry lock && poetry install
poetry env info -p # -> {project-dir}/.envs/demo-<hash>-py3.11 (literal dir under the project)
socket-patch scan --mode agent --yes # any pkg:pypi/six@1.16.0 patch (a local mock API was used here)
poetry run python -c 'import six; print(six.__file__, getattr(six, "SOCKET_PATCHED", 0))' # -> .../{project-dir}/.envs/.../six.py 0
grep -c SOCKET_PATCHED /usr/lib/python3/dist-packages/six.py # -> 1
socket-patch vex --output vex.json # exit 0, 1 statement, not_affectedControl: virtualenvs.path = ".envs" in the same setup patches Poetry's env (poetry run sees the patch) and leaves the system copy untouched.
Expected vs actual
- Expected: docs/testing/poetry-compatibility.md ("Mode notes") says agent mode follows Poetry's
EnvManager.get(), with placement "reproduced without running Poetry, fromPOETRY_*, the project'spoetry.toml, the userconfig.toml…". Sovirtualenvs.pathshould resolve the way Poetry resolves it: an unknown{key}kept literally on Poetry ≥ 1.2 (empty on 1.1), and a relative result taken against the cwd. The env Poetry actually uses gets patched, or, if it can't be found, nothing outside the project is written andvexrefuses. - Actual:
{project-dir}is replaced with the cwd, the env is missed, the global copy is patched, and VEX attests.
Matrix (Linux, main 045d7ec)
| Poetry | {project-dir}/.envs |
.envs (control) |
|---|---|---|
| 1.1.15 | fail (env at /.envs/...; system six patched, vex attests), 2/2 runs |
not run |
| 1.8.5 | fail (env at <proj>/{project-dir}/.envs/...), 2/2 runs |
pass (r5) |
| 2.5.1 | fail (same), 2/2 runs | pass |
| macOS / Windows | untested (probe branches unavailable this run) |
Suspect code
crates/socket-patch-core/src/crawlers/python_crawler.rs:1003-1005:.replace("{project-dir}", &cwd.to_string_lossy()). Poetry has no such key. Other unknown{…}keys aren't modelled either; only{cache-dir}is a real Poetry substitution here.crates/socket-patch-core/src/crawlers/python_crawler.rs:741: the doc comment calls{project-dir}one of "Poetry's placeholders".- Unit test
python_crawler.rs:3550-3556asserts the incorrect expansion. - The global fallback in
find_local_venv_site_packages_withis what turns the miss into a write outside the project (documented behaviour, but it amplifies this).
Activity
mikolalysenko commentedon Oct 2, 2026
[agent] Triage: priority:p1 (Poetry). Confirmed on main
045d7ec:poetry_virtualenvs_pathincrates/socket-patch-core/src/crawlers/python_crawler.rs(~L1003-1005) does.replace("{project-dir}", cwd), and the unit test near L3551 asserts that expansion. No open or merged PR covers this; it's a separate cause from the open PDM/uv issues.Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] New information from the Poetry bug-hunt routine (ledger #311), main
045d7ec, Linux. This is the opposite side of the same placeholder-parity bug, inpoetry_virtualenvs_path(crates/socket-patch-core/src/crawlers/python_crawler.rs:1002-1005).Poetry's
Config.process()doesn't use a fixed list of placeholders. It replaces any{key}withself.get(key), and leaves the{key}literal only when that config key is unset. Starting with Poetry 2.1,data-diris a real config key (default~/.local/share/pypoetryon Linux;POETRY_DATA_DIR). So{data-dir}is expanded invirtualenvs.path, and also insidecache-dir, whichvirtualenvs_paththen builds on. socket-patch only replaces{cache-dir}(and the non-existent{project-dir}). It also never processes placeholders insidecache-diritself.Each cell ran twice. Setup: real
poetry installofsix==1.16.0, thenpip install six==1.15.0into the env Poetry chose, so the batch request shows whether the env was crawled. Thensocket-patch scan --mode agent --json.six@1.15.0[virtualenvs] path = "{data-dir}/venvs"~/.local/share/pypoetry/venvs/demo-…cache-dir = "{data-dir}/cache"~/.local/share/pypoetry/cache/virtualenvs/demo-…path = "{cache-dir}/venvs"(control)~/.cache/pypoetry/venvs/demo-…{data-dir}form./{data-dir}/…in the project (nodata-dirkey before 2.1)Config.create().get("data-dir")returnsNoneon 1.8.5 and 2.0.1, and~/.local/share/pypoetryon 2.1.1, 2.2.1, 2.3.3, 2.4.3 and 2.5.1. A fix for this issue is probably best done by mirroringprocess(): substitute every{key}that names a set config key (cache-dir, anddata-diron Poetry ≥ 2.1), leave anything else literal, and apply the same processing tocache-dir.Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Claiming this issue (with #640; shared root cause: socket-patch doesn't model Poetry's own location and config rules. It has no
locations.data_dir()(POETRY_HOME,POETRY_DATA_DIR, platformdirs user data dir), and it doesn't mirrorConfig.process(){key}substitution). Branch: agent/fix-poetry-data-dir-model. Claim-ID: 2026-10-03T03:21:09Z-cc9545Generated by Claude Code
mikolalysenko commentedon Oct 3, 2026
[agent] Draft PR: #644
Generated by Claude Code
Resolve Poetry paths the way Poetry does
poetry: run 13 ledger (#866 filed; #608/#640 verified; #671/#450 stil…