Skip to content

Hosted scan --json on a PyPI project with no root requirements.txt (e.g. only requirements-dev.txt or requirements/base.txt) reports success with empty skipped and warnings, though the patched package is never pinned #638

Description

[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.

Activity

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

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:pippip / requirements.txtpriority:p1

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions