Skip to content

A bare scan or get on a project holding both vendored packages and hosted pins silently converts every hosted pin to vendored (exit 0, no warning), so it undoes a partial hosted takeover #1437

Closed

Description

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

Summary

Since #1317 (the fix for #1088), a bare scan or get with no --mode picks its mode from the project's state (mode_from_project_state). That function only looks at the vendor ledger and the manifest. Hosted pins aren't counted, because hosted mode keeps no ledger. So a project that holds both vendored packages and hosted pins is classed as plain "vendored". The bare run then does a hosted → vendored takeover of every hosted pin: it restores the registry entry, commits a new .socket/vendor/npm/<uuid>/*.tgz, and adds a ledger entry. It exits 0 with status: success and no warning code in --json. Human output does admit it: Note: pkg:npm/minimist@1.2.2 was hosted; restored its upstream registry entry (bun.lock) before vendoring (mode takeover).

The CLI contract says the opposite (CLI_CONTRACT.md, "Mode resolution"): "Switching modes is always explicit: … the hosted → vendored takeover only under --mode vendored, and a bare scan/get never converts a project." docs/migrating-to-v5.md and docs/usage.md say the same.

Impact

socket-patch creates this mixed state itself, in two documented ways:

  1. A partial vendored → hosted takeover (Make the vendored-to-hosted takeover atomic #1039). scan --mode hosted on a vendored project, where one purl can't be hosted (grant pending_build, denied, or no sha512), keeps that purl vendored and moves the rest to hosted. The run exits 0 and reports redirect_takeover_reverted_vendored. The next bare scan (the quick-start command, and what a CI job typically runs) silently undoes the takeover the user explicitly asked for: every hosted package goes back to vendored.
  2. Per-package modes. get <pkg> --mode vendored plus get <other> --mode hosted (ledger run 25 covers these mixed per-package modes). Any later bare scan or bare get <other> converts the hosted package to vendored.

The bytes stay patched, so this isn't a security hole. But it's an unrequested mode switch, the mirror image of #1088. It adds committed tarballs (repo size, review noise, the #831 / #1116 .gitignore hazards), changes what the lockfile resolves, and gives --json consumers no signal. It isn't specific to Bun: the same thing happens with npm (package-lock.json; it also deletes the hosted run's .npmrc), because the mode decision is ecosystem-agnostic.

Repro (Linux, Bun 1.4.3, main 4e146dd)

I used a local mock of the public patch proxy (SOCKET_PROXY_URL / SOCKET_PATCH_SERVER_URL / SOCKET_VENDOR_URL) that serves free patches for left-pad@1.3.0 (uuid 1111…) and minimist@1.2.2 (uuid 2222…).

mkdir mixed && cd mixed && git init -q
echo '{"name":"t","version":"1.0.0","dependencies":{"left-pad":"1.3.0","minimist":"1.2.2"}}' > package.json
echo node_modules/ > .gitignore
bun install && git add -A && git commit -qm init

# Path 1: partial takeover (#1039)
socket-patch scan --mode vendored --yes --json      # both vendored
git add -A && git commit -qm vendored
# (the service answers left-pad's grant with pending_build)
socket-patch scan --mode hosted --yes --json        # exit 0: left-pad kept vendored, minimist hosted,
                                                    # warnings: redirect_takeover_reverted_vendored
git add -A && git commit -qm "move to hosted"
socket-patch scan --yes --json                      # exit 0, status success, warnings []
git status --short
#  M .socket/vendor/state.json
#  M bun.lock          ("minimist": ["minimist@.socket/vendor/npm/2222…/minimist-1.2.2.tgz", …])
# ?? .socket/vendor/npm/22222222-2222-4222-8222-222222222222/

# Path 2: per-package modes
socket-patch get left-pad --mode vendored --yes
socket-patch get minimist --mode hosted --yes
git add -A && git commit -qm mixed
socket-patch scan --yes --json                      # same: minimist converted hosted -> vendored

scan --dry-run --json predicts the conversion: {"action":"verified","purl":"pkg:npm/minimist@1.2.2","details":{"mode":"vendored"}}, also with no warning.

Expected vs actual

  • Expected (CLI_CONTRACT.md "Mode resolution", docs/migrating-to-v5.md): a bare scan / get never converts a project. When a project holds vendored entries and hosted pins, the run should either keep each package in the mode it's already in, or refuse as ambiguous the way agent + vendored does (mode_ambiguous, exit 2, asking for --mode). Either way, it shouldn't run the hosted → vendored takeover.
  • Actual: exit 0, status: success, warnings: [], and every hosted pin is converted to vendored. Human mode prints only the takeover note.

Matrix (Linux; "converts" = the bare run vendors the hosted pin, exit 0, no JSON warning)

PM Lock How the mixed state was created bare scan bare get minimist
Bun 1.4.3 text bun.lock v2 partial --mode hosted takeover (#1039) converts —
Bun 1.4.3 bun.lockb partial --mode hosted takeover (#1039) converts —
Bun 1.4.3 text bun.lock v2 get --mode vendored + get --mode hosted converts converts
Bun 1.4.3 bun.lockb get --mode vendored + get --mode hosted converts —
Bun 1.2.23 text bun.lock v1 get --mode vendored + get --mode hosted converts (also in human mode, --dry-run predicts it) —
npm 10.9 package-lock.json get --mode vendored + get --mode hosted converts (and deletes .npmrc) —
Bun 1.4.3 workspace member, root vendored — refused redirect_workspace_lockfile_elsewhere, nothing written (fine) —

After each conversion, a fresh clone's bun install --frozen-lockfile passes and is patched. The harm is the unrequested mode switch, not lost patch bytes. macOS and Windows weren't run; this is platform-independent mode-selection logic.

First bad commit: 3bcb267f (#1317) for this direction. On its parent 56ccbfc3, the same mixed project was converted the other way by a bare scan: left-pad was taken from vendored to hosted, the ledger and artifact were deleted, exit 0 (#1088 itself). So mixed hosted + vendored projects have never been left alone by a bare run. #1317 only flipped which half gets converted.

Suspect code

  • crates/socket-patch-cli/src/commands/mod.rs:81 mode_from_project_state: vendored = ledger non-empty and agent = manifest has uncovered records. The lockfiles' hosted pins never count as hosted state, and the doc comment says so ("neither → hosted (hosted mode keeps no ledger of its own)"). The (true, false) arm returns Vendored even when hosted pins are present.
  • The vendored flow then runs the hosted → vendored takeover for every hosted pin it meets (Note: … was hosted; restored its upstream registry entry … (mode takeover)), with no check for whether the mode was explicit.
  • Possible direction: detect hosted pins (the same read-only discovery rollback / list already use), and treat vendored + hosted as mode_ambiguous. Or let the vendored flow skip the takeover leg when the mode wasn't explicit.

Related: #1088 / #1317 (the vendored → hosted direction), #1039 (partial takeover).

Activity

  1. mikolalysenko commented on Oct 12, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (root cause: mode_from_project_state counts only the vendor ledger and manifest, never hosted pins, so a mixed vendored + hosted project is classed vendored and the bare run takes over every hosted pin). Branch: agent/fix-bare-mode-mixed-hosted-vendored. Claim-ID: 2026-10-12T17:20:58Z-dccf60


    Generated by Claude Code

  2. mikolalysenko commented on Oct 12, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft PR: #1440


    Generated by Claude Code

  3. added 3 commits that reference this issue on Oct 12, 2026
    1601aa1
    155f868
    0a72a49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions