[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Take a pip project whose venv holds a package with a hosted patch but which has no root requirements.txt. Its pins live in requirements-dev.txt or requirements/base.txt, which are common layouts, or it has no requirements file at all. scan --mode hosted --json then exits 0 with status: "success" and redirect: {redirected: 0, skipped: [], warnings: []}. Nothing in the JSON envelope says the patch was not applied.
Human output does say it: No patches could be switched to hosted: pkg:pypi/six@1.16.0: no lockfile entry pinning it could be rewritten. That line comes from the unconfirmed list in scan/hosted.rs, which only goes to stderr.
Every other ecosystem emits a stable JSON warning for the same case: redirect_npm_no_lockfile, redirect_pnpm_no_lockfile, redirect_vlt_no_lockfile, redirect_composer_no_lockfile, redirect_gem_no_gemfile, redirect_maven_no_pom and redirect_golang_no_go_mod. PyPI has no such code.
Impact
A CI job or wrapper that reads --json can't tell this case from a real success unless it compares packagesWithPatches with redirected itself. vex correctly doesn't attest the package, but the scan gives no machine-readable reason why. Meanwhile pip keeps installing the unpatched release from requirements-dev.txt.
Repro
This uses a local mock of patches/batch, by-package, view and package plus the hosted wheel. It's the same shape as mode_migration_pypi.rs::mount_hosted_api.
python3 -m venv /tmp/v && /tmp/v/bin/pip install six==1.16.0
mkdir proj && cd proj
printf 'six==1.16.0\nidna==3.7\n' > requirements-dev.txt # or requirements/base.txt, or no file at all
VIRTUAL_ENV=/tmp/v socket-patch scan --mode hosted --yes \
--api-url $MOCK --org test-org --api-token fake --patch-server-url $MOCK --json
Actual output (main 045d7ec):
"status": "success", "packagesWithPatches": 1,
"redirect": {"mode": "hosted", "redirected": 0, "rewrittenFiles": [], "skipped": [], "warnings": [], "dryRun": false}
The exit code is 0. The same run without --json prints the "no lockfile entry pinning it could be rewritten" line on stderr.
Expected vs actual
- Expected: CLI_CONTRACT.md (hosted scan) says rewriter warnings carry stable
redirect_* codes, and that a missing manifest or lock is reported once per run. Examples are redirect_npm_no_lockfile and "redirect_composer_no_lockfile / redirect_gem_no_gemfile (composer / gem: neither manifest nor lock present — once per run …)". The comment above unconfirmed in scan/hosted.rs says such a package is "listed so it never vanishes silently". So a PyPI package that is granted but unpinned should appear in redirect.warnings[] (for example redirect_pypi_no_lockfile, naming the files that were looked for) or in redirect.skipped[].
- Actual: JSON consumers see a plain success.
A related case that already works: when a root requirements.txt exists but doesn't pin the package, the JSON correctly carries redirect_requirements_entry_not_found. Only the case with no Python manifest at all is silent.
Matrix
| OS |
pip / Python |
Layout |
Reproduces |
| Linux |
26.2.1 / CPython 3.13 |
requirements-dev.txt only |
yes (2/2) |
| Linux |
26.2.1 / CPython 3.13 |
requirements/base.txt only |
yes |
| Linux |
26.2.1 / CPython 3.13 |
no requirements file |
yes |
| Linux |
20.3.4 / CPython 3.8 |
requirements-dev.txt only |
yes |
| Linux |
26.2.1 / CPython 3.13 |
root requirements.txt without the pin |
no (redirect_requirements_entry_not_found is emitted) |
macOS and Windows weren't probed. The JSON assembly doesn't depend on the OS.
First bad version: none found. The published v4.0.0 (PyPI socket-patch==4.0.0) behaves the same way, so this isn't a regression.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:746: the requirements rewriter only runs if files.contains_key("requirements.txt"), and nothing PyPI-side warns when no Python manifest or lock is present. Compare the npm branch at mod.rs:819-857, which pushes redirect_npm_no_lockfile.
crates/socket-patch-cli/src/commands/scan/hosted.rs:1416-1440: unconfirmed purls are passed only to format_unredirected (stderr), never to the JSON redirect object.
Reading only the root requirements.txt is documented and isn't the bug. The missing machine-readable signal is.
[agent] Found by the scheduled pip / requirements.txt bug-hunt routine (ledger #309).
Summary
Take a pip project whose venv holds a package with a hosted patch but which has no root
requirements.txt. Its pins live inrequirements-dev.txtorrequirements/base.txt, which are common layouts, or it has no requirements file at all.scan --mode hosted --jsonthen exits 0 withstatus: "success"andredirect: {redirected: 0, skipped: [], warnings: []}. Nothing in the JSON envelope says the patch was not applied.Human output does say it:
No patches could be switched to hosted: pkg:pypi/six@1.16.0: no lockfile entry pinning it could be rewritten. That line comes from theunconfirmedlist inscan/hosted.rs, which only goes to stderr.Every other ecosystem emits a stable JSON warning for the same case:
redirect_npm_no_lockfile,redirect_pnpm_no_lockfile,redirect_vlt_no_lockfile,redirect_composer_no_lockfile,redirect_gem_no_gemfile,redirect_maven_no_pomandredirect_golang_no_go_mod. PyPI has no such code.Impact
A CI job or wrapper that reads
--jsoncan't tell this case from a real success unless it comparespackagesWithPatcheswithredirecteditself.vexcorrectly doesn't attest the package, but the scan gives no machine-readable reason why. Meanwhile pip keeps installing the unpatched release fromrequirements-dev.txt.Repro
This uses a local mock of
patches/batch,by-package,viewandpackageplus the hosted wheel. It's the same shape asmode_migration_pypi.rs::mount_hosted_api.Actual output (main
045d7ec):The exit code is 0. The same run without
--jsonprints the "no lockfile entry pinning it could be rewritten" line on stderr.Expected vs actual
redirect_*codes, and that a missing manifest or lock is reported once per run. Examples areredirect_npm_no_lockfileand "redirect_composer_no_lockfile/redirect_gem_no_gemfile(composer / gem: neither manifest nor lock present — once per run …)". The comment aboveunconfirmedinscan/hosted.rssays such a package is "listed so it never vanishes silently". So a PyPI package that is granted but unpinned should appear inredirect.warnings[](for exampleredirect_pypi_no_lockfile, naming the files that were looked for) or inredirect.skipped[].A related case that already works: when a root
requirements.txtexists but doesn't pin the package, the JSON correctly carriesredirect_requirements_entry_not_found. Only the case with no Python manifest at all is silent.Matrix
requirements-dev.txtonlyrequirements/base.txtonlyrequirements-dev.txtonlyrequirements.txtwithout the pinredirect_requirements_entry_not_foundis emitted)macOS and Windows weren't probed. The JSON assembly doesn't depend on the OS.
First bad version: none found. The published v4.0.0 (PyPI
socket-patch==4.0.0) behaves the same way, so this isn't a regression.Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:746: the requirements rewriter only runsif files.contains_key("requirements.txt"), and nothing PyPI-side warns when no Python manifest or lock is present. Compare the npm branch atmod.rs:819-857, which pushesredirect_npm_no_lockfile.crates/socket-patch-cli/src/commands/scan/hosted.rs:1416-1440:unconfirmedpurls are passed only toformat_unredirected(stderr), never to the JSONredirectobject.Reading only the root
requirements.txtis documented and isn't the bug. The missing machine-readable signal is.