From a3715f4c941b004dc46010c0c5bc03305cb9f13e Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 10 Nov 2026 19:11:40 -0500 Subject: [PATCH] chore(release): add the 5.1.0 notes for the full-review fixes (#2052) The owner asked for the changes of #2052 to be written into the 5.1.0 section (5.1.0 is stamped on main and not tagged yet). They are on this release branch because AGENTS.md lets only a `release/v*` branch edit CHANGELOG.md; the same text was restored out of #2052 by a repository routine for that reason. 82 entries are added inside `## [5.1.0]`, in its existing headings and ecosystem groups (Cargo and Composer groups are new under Changed); nothing else in the file changes. Each entry was written from the commit messages and the CLI_CONTRACT.md diff of #2052 and states any exit code it changes, any statement `vex` no longer makes, any file the tool now writes and any new refusal. Merge after #2052: the entries describe code that lands there. Co-Authored-By: Claude Fable 5.2 --- CHANGELOG.md | 648 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 648 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bd765b6..384c9a538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,34 @@ a version without a non-empty section in this file. rebuild, with a `native_extension_not_rebuilt` warning, and `vex` does not attest them. Hosted copies are unaffected, since RubyGems compiles the patched gem itself (#1996, #1998). + - **Hosted mode replaces the gems the project already has installed.** + Bundler keeps a gem it already holds whatever source the lock names, so + after `scan` / `get --mode hosted` repointed `Gemfile.lock`, + `bundle install` printed "Bundle complete!" over the unpatched copy. The + run now replaces those copies itself with the patched `.gem` the lock + pins, checked against that sha256 first. It writes files earlier + releases never touched: an installed gem whose gem home is inside the + project (its gem dir, the home's cached `.gem` and, for a gem with a + native extension, the extension dir that RubyGems rebuilds in a staging + home), and a stale archive in Bundler's cache dir (`vendor/cache`, a + committed file the run lists with the files to commit). `rollback`, + `remove` and the withdrawn-patch restore do the same with the upstream + gem, downloading it from rubygems.org when no copy is on disk. No + `bundle` or `gem` command runs, `--dry-run` writes and downloads + nothing, `--offline` uses only an archive already on disk, and the exit + code never changes: a copy that can't be refreshed is left as it was. + Gem homes outside the project (system gems, `~/.gem`, an rbenv or asdf + home) and bundle paths that resolve outside it are left alone unless + `--gem-refresh-shared` (env `SOCKET_GEM_REFRESH_SHARED`) is given, and + `--no-gem-install-refresh` (env `SOCKET_NO_GEM_INSTALL_REFRESH`) turns + the refresh off. Each replaced copy is a + `redirect_gem_install_refreshed` or `upstream_gem_install_refreshed` + note (`redirect_gem_install_would_refresh` / + `upstream_gem_install_would_refresh` on a dry run). What is still stale + is one human warning with the remedy as a command + (`redirect_gem_stale_install`, and the new `upstream_gem_stale_install` + after an unwind), and the hosted `Next steps:` of a Bundler-only run say + there is nothing to reinstall once the copies were replaced (#2052). - **Go.** - Run from the directory holding `go.work`, `apply`, `vendor` and hosted scans write one replace into `go.work` for a module the workspace @@ -71,6 +99,32 @@ a version without a non-empty section in this file. is on offer, the same run pins it. A failed lookup keeps every pin and warns `redirect_withdrawn_check_failed`. A restore that can't run warns `redirect_withdrawn_restore_failed` (#1592). + - `scan` names the advisories a run fixes and the ones it leaves + unpatched. One patch is applied per package version while the results + table lists the ids of every candidate patch, so every mode now prints, + under its result line, `: fixes ; still unpatched: ` for + each package it patched (`would fix` / `would stay unpatched` on a dry + run), then one closing line saying that upgrading the package fixes the + rest. `scan --json` carries the same on each `packages[]` entry the run + selected a patch for, in the new `selectedUuids`, `fixedIds` and + `unfixedIds` keys. An advisory is reported fixed only when the selected + patch names it (#2052). + - An agent-mode run that records a patch (`scan --mode agent`, agent-mode + `get`) ends with a `Next steps:` block: commit `.socket/`, and run + `socket-patch apply` after every install, locally and in CI, naming the + install command of each patched ecosystem (`bundle install`, + `pnpm install`, `uv sync`, …). After `--save-only` it says to run + `apply` now, and a Go patch adds `go.mod` to the commit step and needs + no re-apply. Nothing is printed for a dry run, a failed or global run, + or under `--silent` / `--json` (#2052). + - `--ecosystems` and `socket.yml` `patches.ecosystems` accept the names + the output prints and everyday ones, in any case: `python`, `ruby`, + `go`, `php`, `pip`, `rubygems`, `rust`, `crates`, `java`, `gradle`, + `sbt`, `dotnet`, `packagist`, `jsr`, and `node` / `pnpm` / `yarn` / + `bun` / `vlt` for `npm`. Before, `-e go` and `-e python` were rejected + although the CLI prints "(5 go)", and `socket.yml` answered `go` with + "did you mean `gem`?". Any other name is a usage error that lists the + slugs (#2052). ### Changed @@ -123,6 +177,175 @@ a version without a non-empty section in this file. them all. A failed delete is now a `cleanup_failed` warning in `rollback`, `remove`, `vendor --revert` and `scan --prune` (#1610, #1683). + - **A bare `scan` covers every project in the git checkout.** With no + PATH, a hosted or vendored `scan` (the default mode included) now scans + `--cwd` and every other project directory below it in the same git + checkout, each as if it had been named: its own lockfiles, mode and + `.socket/`, under a `== ==` heading (`== . ==` for `--cwd`), with + one `--max-new-patches` budget and the worst exit code. A scan from a + monorepo root therefore rewrites lockfiles in subdirectories that + earlier releases left alone: they scanned the root only, or printed "No + packages found." and exited 0 when the root had no lockfile. A project + directory is one holding an npm-family, PyPI, Cargo, RubyGems, Go or + Composer lockfile; Maven, Gradle, sbt and NuGet projects are still + scanned only as `--cwd` or a named PATH. The search honours git's + ignore rules (a `.ignore` file is not one of them), follows no link, + never enters `node_modules`, `.git`, `.socket`, `.yarn` or `vendor`, + never enters a git submodule or a nested clone (another repository: + run socket-patch inside it), and leaves out a workspace member whose + lock the workspace root's run reads. `socket.yml` path + filters and the built-in test and fixture ignores apply, with one note + counting what they left out. Nothing changes outside a git checkout, + with a PATH, under `--global` / `--global-prefix` or in agent mode. + `--json` and `--vex` still scan `--cwd` alone and name the directories + they left in a new `projects_not_scanned` warning (#2052). + - `list` and a bare `rollback` cover what such a scan patched: the project + at `--cwd` and every patched project directory below it, under the same + headings. `list` adds one total line and, under `--json`, a top-level + `projects[]` beside the unchanged envelope for `--cwd`. `rollback` with + no target now also rolls back the directories below, after one + confirmation that names them, and exits with the worst code; `--dry-run` + previews them all. It reaches exactly as far as a bare scan from the + same directory: from an agent-mode project, whose scan covers that + project alone, it rolls back that project alone and warns + `projects_not_rolled_back`. `rollback --json` never changes a directory + other than `--cwd`: it rolls back `--cwd` alone, as before, with the + same warning, and with nothing to undo there it fails as it always did + (`manifest_not_found`, exit 1). `rollback `, + `remove`, `vex`, `repair` and `apply` still act on one project and keep + their exit codes. When `--cwd` holds nothing for them they add a note + (`patched_projects_below` under `--json`) naming the directories below + and the `--cwd ` command for each; `apply --silent` does not search + (#2052). + - `scan` and `get` report "no patches" only when the patch service said + so. An answer from another host (a mistyped `--proxy-url` or + `--api-url`, a captive portal, a forward proxy), a 404 the service did + not write, and the API's "Organization not found" for a mistyped `--org` + are now errors; so is a lookup of the token-less per-package fallback + that ends in a 5xx or in the proxy's "Patch API is not configured". + Before, each read as "No patches available for installed packages." or + "No patches found" with exit 0. `scan` goes on with the packages that + were answered and reports the rest with a `patch_lookup_failed` warning + and a `failed` event each: the run is `partialFailure` and exits 1 in + every mode, and exits 1 with `api_batch_failed` when no package was + answered. `get` exits 1 naming the host or the org slug to check. A + lookup still throttled after its retries is reported the same way + instead of failing its whole batch (#2052). + - Manifest records no crawler can look for are named instead of dropped. A + record of an ecosystem this CLI doesn't support (one a newer + socket-patch wrote, or a mis-cased type such as `pkg:NPM/…`) is a + `skipped` / `unsupported_ecosystem` event and one warning in `apply`, + `apply --check` and `rollback`; it never changes the exit code, and + `apply --check` no longer counts it as in sync. A key that is not a purl + at all now fails: `apply` exits 1 naming it while the other patches + still apply, `apply --check` reports drift (`invalid_purl`) and + `rollback` a failed entry. `remove` refuses either kind before any + change (exit 1) unless `--skip-rollback` is given. Before, `apply` + printed "No patches to apply." and exited 0, and `remove` dropped the + record without restoring anything (#2052). + - `repair` verifies the blobs it finds. Every referenced blob is + re-hashed, and one whose bytes changed on disk (a `core.autocrlf` + checkout of a committed `.socket/blobs/`, a truncated file) is + downloaded again and replaced. Before, `repair` said "All blobs are + present locally" and every `apply` failed on the blob. + `repair --offline` now exits 1 when a referenced blob is missing or + corrupt, with a `failed` event per blob (`offline_missing_sources` / new + `blob_corrupt`) and an error list `--silent` doesn't mute; it used to + print "Repair complete." and exit 0. `apply` and `rollback` name a + corrupt blob by its path and say to run `socket-patch repair` (#2052). + - A failed checksum-sidecar fixup now fails the package. When `apply` or + `rollback` can't bring cargo's `.cargo-checksum.json`, NuGet's + `.nupkg.metadata` or the Maven and Coursier checksum files in line with + the files it wrote, the files are put back, the event is `failed` + ("sidecar fixup failed" / "sidecar resync failed") and the run exits 1: + an apply leaves the package unpatched and a rollback leaves it patched. + Before, the package counted as applied with exit 0 and only a + `sidecar_fixup_failed` record under `--json`, which for cargo left a + crate `cargo build` refuses. A run that finds every file already patched + now rechecks and repairs those sidecars, and `apply` prints sidecar + notes and warnings on stderr without `--json` (#2052). + - The programs socket-patch runs (package managers and their + version-manager shims, interpreters, `git`, a gem's `extconf.rb` and + compiler) no longer inherit Socket credentials or settings. Their + environment is stripped of the API token under every name a Socket tool + reads it from (`SOCKET_API_TOKEN`, `SOCKET_CLI_API_TOKEN`, + `SOCKET_SECURITY_API_TOKEN` and the `_API_KEY` spellings), + `SOCKET_CLI_CONFIG`, `SOCKET_CLI_API_PROXY`, every flag-bound `SOCKET_*` + variable and the Socket registry override URLs. Names are matched + exactly, so `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, `NPM_TOKEN` and + other registry credentials still reach the child (#2052). + - `vex --no-verify` (also `--vex-no-verify` and `SOCKET_VEX_NO_VERIFY`) + says what it did not check. Each statement emitted without on-disk + verification ends its impact statement with `; not verified on disk`, + and the run warns `vex_not_verified` with the count. Before, an + agent-mode patch whose files were unpatched or missing was attested + `not_affected` with the same text as a verified one, with no warning. A + verifying run's document is unchanged (#2052). + - `vex` attests a vendored patch only while a lockfile reference wires its + package to its artifact. A vendor ledger entry is no longer kept alive + by text in the files the ledger names: a `pnpm.overrides` entry left in + `package.json` after `pnpm-lock.yaml` went back to the registry, a + Gemfile `path:` beside a reverted lock, or any file that merely spells + the vendored directory now gets `vendor_unwired` and no statement. + `vendor --check` applies the same rule; `scan --prune` still keeps such + an entry (#2052). + - A lockfile URL counts as a Socket patch reference only when every client + reads its host alike and the patch server serves its path. A URL with a + backslash, whitespace or a control character + (`https://patch.socket.dev\@evil.example/…`), a percent-encoded `/` or + `\`, or a path other than `patch//…`, + `patch-registry//…` or `gopatch/…` (under any prefix on a + `--patch-server-url` deployment) is no longer attested by `vex` or + managed by `list`, `rollback`, `remove` and `scan` (#2052). + - When two lockfiles wire one package to the same hosted patch and one of + them lacks the integrity pin hosted mode always writes, `vex` no longer + attests the patch from the lockfiles alone: the pinned lock doesn't + vouch for the other (#2052). + - `vex` no longer accepts a vendored reference whose artifact name carries + a `%` (`.socket/vendor/npm//%66oo-1.0.0.tgz`): npm, yarn and pnpm + open the literal file while pip percent-decodes it, so it names no + single file to verify. No vendor backend writes such a name (#2052). +- **RubyGems.** + - **Hosted mode keeps your `gem` declaration and always converges the + lock.** `scan` / `get --mode hosted` used to replace a gem's Gemfile + declaration with a `source "" do … end` block holding + the exact version, which lost the version requirement, quoting and + comment for good, and it left a lock without `CHECKSUMS` alone, so + frozen installs on Bundler older than 2.6 exited 16. It now appends one + option, `, source: ""`, to the declaration and changes + nothing else. That covers parenthesized calls, and the multi-line, + `if`-modified and `;`-joined declarations that were refused before. A + transitive gem gets one line at the end of the file + (`gem "", "", source: "", require: false`, marked + `# added by socket-patch (transitive dependency)`). `Gemfile.lock` is + always edited to the bytes the Bundler that wrote it produces, now also + a lock with no `CHECKSUMS` section, which earlier releases did not + write, so frozen and deployment installs accept the pair as committed. + `redirect_gem_no_checksums_section` is raised once per run instead of + once per gem, and a lock written by Bundler older than 2.2 warns the new + `redirect_gem_merged_lock_layout`. A gem whose pair can't be made + consistent is skipped with neither file changed (new + `redirect_gem_dependency_not_locked`, `redirect_gem_lock_unconvergeable` + and `redirect_gem_inconsistent_plan`), and a gem inside your own + `source` / `git` / `github` / `path` block is now refused with + `redirect_gem_source_option` where it used to get a nested Socket block. + `rollback` and `remove` take the option or the added line off again, + byte for byte. Block pins written by 5.0 / 5.1 stay recognized: a + re-scan keeps the block and now converges its lock, and their unwind + still restores the exact pin, with a new + `upstream_gem_declaration_pinned` warning. The scan prints one line + naming the declarations it changed (#2052). + - `vex` attests a gem under a Socket section of `Gemfile.lock` only when + the manifest beside the lock gives the gem that source: the `source:` + option, the 5.0 / 5.1 `source … do` block, or `path:` to the vendored + directory. With a lock that names the Socket source and a Gemfile that + does not, Bundler fails a frozen install and an unfrozen one rewrites + the lock and installs the upstream gem (checked on Bundler 1.17, 2.1, + 2.4, 2.5 and 4.0), so that state is no longer attested: a hosted pin + reads `patched_ref_unattributable` and a vendored one `vendor_unwired`. + Both stay among the pins `rollback` and `remove` unwind. A declaration + whose `source:` / `path:` is not a plain string literal, and a lock + checked out without its manifest, are withheld too (#2052). - **npm family.** - A package the project patches itself (pnpm `patchedDependencies`, a yarn `patch:` descriptor or lock locator, patch-package files), @@ -134,6 +357,96 @@ a version without a non-empty section in this file. withholds the pnpm lock pins whatever `gitBranchLockfile` says (`vex_pnpm_git_branch_lockfile`), and an unreadable lock or patch evidence outside the repository fails closed (#2018). + - `vex` no longer trusts a lock the project tells its package manager not + to read: npm `package-lock=false` (or `shrinkwrap=false`) in `.npmrc`, + yarn 1 `--install.no-lockfile` in `.yarnrc`, a yarn berry + `lockfileFilename` naming another file, or pnpm's `lockfile` setting + turned off. A hosted pin in such a lock is attested only from an + installed tree that verifies, and a vendored pin is omitted with the new + `vex_lock_not_read`, except a pnpm pin that the override map pnpm reads + still routes to the artifact. `list`, `rollback`, `remove` and + `vendor --check` still see the pin (#2052). + - `vex` withholds a pnpm pin (`patched_ref_unattributable`) when the lock + installs a second copy of the same `name@version`: an entry from a git + remote or a URL tarball, a `file:` copy whose `package.json` can't be + read, or, in lockfileVersion 5 / 6 locks, another registry key of the + release. A root-relative `.socket/vendor/npm/…` path in a per-member or + Rush lock is reported as `patched_ref_invalid` instead of attested, + since pnpm resolves it against that lock's own directory (#2052). + - Run from a pnpm or yarn 1 workspace member, `vex` reads the member's own + stray `pnpm-lock.yaml` or classic `yarn.lock` as absent, because the + package manager installs the member from the workspace root: its pins + are not attested, and one `patched_ref_unattributable` names the root to + run from. Per-member pnpm locks (`sharedWorkspaceLockfile: false`) and a + berry lock in a member are read as before (#2052). + - Hosted `scan` and `get` run from such a member refuse instead of + pinning a lock nothing installs from. A `pnpm-lock.yaml` inside a member + that `pnpm-workspace.yaml` lists is never read by pnpm, which installs + the member from the workspace's shared lock (checked with pnpm 10.34 and + 11.27), and yarn 1 never reads a `yarn.lock` inside a `package.json` + workspace member. Earlier releases wrote the pin there and reported the + package switched while installs kept the unpatched copy. The run now + exits 1 with `redirect_workspace_lockfile_elsewhere`, names the + workspace root to run from and writes nothing. A workspace that sets + `sharedWorkspaceLockfile: false`, a directory the workspace does not + list and a yarn berry lock in a member are pinned as before (#2052). +- **PyPI.** + - Vendored uv refuses a patch for another version than `uv.lock` resolves + (`pypi_uv_lock_package_missing`, exit 1), before any download and in the + `--dry-run` preview. Earlier releases wired it with exit 0 by rewriting + the lock entry's `version`, so a patch left behind by a dependency bump + moved the lock down to the older release and `uv sync` installed it. + With patches for two releases, the one for the locked release is wired + and the other is refused. A project an earlier release vendored that way + keeps working and still reverts (#2052). + - `vex` withholds a Python pin that another entry or requirement installs + unpatched. That covers a second entry of the same release in the same + `uv.lock`, `pylock.toml`, `poetry.lock`, `pdm.lock` or `Pipfile.lock` + that resolves from the index (a marker fork, an extras variant, the + other Pipfile category), and a PEP 723 script lock that disagrees with + the project lock: script locks now take part in the cross-lock check. + Re-running `scan` or `vendor` rewires every entry and clears both. A + reference behind an environment marker, extra, dependency group or Hatch + environment is also withheld (`patched_ref_unattributable`) while a + plain requirement can install the same release where the reference + doesn't apply (#2052). +- **Cargo.** + - `vex` attests a Socket-sourced `Cargo.lock` entry only while a + declaration cargo honors routes the crate to it: a live dependency + pinned to the patch registry whose requirement can select that version, + or a live `[patch]` registry pin in the root manifest or + `.cargo/config.toml`. A lock entry nothing routes (a transitive-only + crate, a shadowed `[patch.crates-io]` entry) makes `cargo fetch + --locked` fail and an unlocked build link the upstream crate; `vex` + attested it and now withholds it with `patched_ref_unattributable`. The + entry stays among the pins `rollback` and `remove` unwind. Run from a + workspace member, `vex` attests nothing from the member's own `[patch]` + or lock, which cargo ignores for the workspace root's, and names the + workspace root (#2052). +- **JVM.** + - `vex` withholds a Gradle statement when the derived-cache check of its + patched copy doesn't finish: the purl is omitted as + `gradle_unpatched_copy` with the new + `vex_gradle_derived_cache_check_failed` warning. A check that crashed + used to count as nothing to check (#2052). + - Maven: a patch pinned only through ``, for a + package the root pom does not declare, is weighed against the parent + poms. When a parent in the checkout, a grandparent or an active parent + profile declares the package with a literal version, Maven builds the + upstream jar, so `vex` omits the patch (`vex_maven_parent_declares`). + When a parent cannot be read (``, a parent outside the + checkout, a missing one), the pin is no longer attested from the pom + alone: it reads `package_not_found` until a build has resolved the + patched version. That withholds the pre-build statement in projects + that inherit from a published parent such as + `spring-boot-starter-parent`; the statement returns after a build + (#2052). +- **Composer.** + - `vex` no longer attests a hosted Composer pin from the lock alone when + `composer.json` puts the install outside the project + (`config.vendor-dir`) and an install exists there that the crawler does + not read: the patch is omitted with `composer_install_unread`. Set + `COMPOSER_VENDOR_DIR` to have that tree read and verified (#2052). ### Fixed @@ -191,6 +504,21 @@ a version without a non-empty section in this file. does, skipping comments, `=begin` blocks, heredocs and `__END__`. A commented-out `spec.version` or `spec.name` no longer sets the product purl (#1573, #1582). + - Agent `apply` runs no Ruby for a gem's native-extension rebuild until + the patch's files verify against the installed gem, and none on a dry + run. Before, `apply --dry-run` evaluated the installed gemspec, a Ruby + program that a committed `vendor/bundle` lets the scanned repository + supply. The gemspec is now read as text, a dry run reports the pending + rebuild as a `native_extension_rebuilt` warning, and refusal codes are + unchanged (#2052). + - Interrupting `apply` during a gem's native rebuild (Ctrl-C, SIGTERM) + ends the build with it, instead of leaving `ruby`, `extconf.rb` and the + compiler running into a gem no lock protects. The next `apply`, + `rollback` or `remove` puts the previous build back first, then rebuilds + or restores; before, it deleted the parked build or refused with + "installed by a RubyGems older than 2.2". A gem that another process is + still rebuilding is refused with `native_extensions_unsupported` + (#2052). - **npm family.** - pnpm workspace globs with brace sets and character classes (`packages/{a,b}`, `packages/[ab]*`) are read the way pnpm reads them in @@ -445,6 +773,78 @@ a version without a non-empty section in this file. `rollback`, `apply --check` and `vex` now follow the member's npm alias link (`"lp": "npm:left-pad@1.3.0"`) into the root store. Before, they reported `package_not_installed` (#1494, #1500). + - Vendoring into a pnpm or yarn berry project edits `package.json` in + place instead of re-printing it. Only the `pnpm.overrides`, + build-approval or `resolutions` members change; formatting, escapes, + number spellings (`5.173e28`), duplicate keys, line endings and a BOM + stay, so `vendor` then `vendor --revert` is byte-exact. A yarn berry + revert removes a `resolutions` table only when vendoring created it and + leaves one the project already had, even empty (#2052). + - A yarn berry vendored revert reads `package.json` before it writes + `yarn.lock`. A manifest that doesn't parse (a merge-conflict marker) now + fails with "nothing was reverted" and both files untouched, instead of + leaving the lock restored while `resolutions` still pointed at the + vendored tarball (#2052). + - A vendored pnpm or yarn revert finishes only once nothing resolves + through the artifact. A pnpm revert points dependency references added + after vendoring (a new package or workspace importer) back at the + registry release, and refuses with + `vendor_lock_still_wired_revert_blocked`, writing nothing, when one + can't be restored; before, it deleted the artifact and pnpm failed with + "Broken lockfile". Once a dependency was upgraded past the patched + version, pnpm and yarn reverts remove the artifact and ledger entry with + the new `vendor_revert_dependency_moved_on` warning instead of reporting + the same drift forever (#2052). + - Vendored pnpm no longer corrupts a `packages:` entry whose `deprecated` + message is a block scalar, or one with a peer dependency named + `resolution`. The rebuilt entry is read back before anything is written, + and a lock that doesn't read back is refused with the project untouched + (#2052). + - Hosted pnpm keeps a CRLF `pnpm-workspace.yaml` CRLF when it adds + `trustLockfile: true`. `rollback` restores the file byte for byte and + removes a scaffold hosted mode created in either line ending (#2052). + - Reverting two or more packages vendored into one `bun.lockb` restores + the lock's exact pre-vendor bytes in any order, also for a ledger 5.0 / + 5.1 wrote when one `vendor --revert` run reverts them all. Before, the + first package's tarball path stayed in the lock while the revert + reported success (#2052). + - In a `bun.lockb` workspace, vendoring copies a tarball only into the + members whose dependencies reach the package, instead of into every + member's `.socket/vendor/npm//`. `repair` asks for the same + members, and a revert still removes the copies an earlier release made + (#2052). + - A Bun vendored revert keeps the artifact and ledger entry + (`vendor_revert_residual_reference`) while any `bun.lock` or `bun.lockb` + entry still installs from it. Vendoring now refuses a `bun.lock` that + lists the key it would rewrite more than once, as a merge that kept both + sides leaves it (`vendor_lockfile_version_unsupported`); before, the + revert restored one line and deleted the tarball the other still pointed + at (#2052). + - A vlt `vendor --revert` checks what it leaves behind. An unrecorded lock + edge that still reaches the vendored node leaves the revert alone as + drift, and anything else still naming the uuid directory keeps it with + `vendor_revert_residual_reference`, instead of deleting the artifact + with exit 0. `vendor --revert --dry-run` reports the same keeps as the + real run. A workspace directory that is a symlink is refused + (`vendor_lock_entry_unsupported`) instead of being written through, and + a `vlt-lock.json` whose `nodes` or `edges` section can't be located for + certain is refused before any write + (`vendor_lockfile_version_unsupported`) (#2052). + - The `yarn_classic_workspace_member_install_risk` warning says that a + cold-cache install fails from every workspace member, whether or not it + depends on a patched package, and names the install that works from a + member directory: `yarn install --cwd ` (#2052). + - A broken project file is reported as a broken file. A `package.json` + that isn't valid JSON now reads "not patched, because this project's own + patches for it could not be checked", names the file and the parse error + and says to fix it, instead of advising `--allow-patch-conflicts`. A + `package-lock.json` that isn't JSON warns `npm_lock_invalid` and the + scan ends "No packages found (see the warning)." instead of advising an + install. Both runs still exit 0 (#2052). + - `vex` reads a yarn berry lock that holds a YAML complex-key entry + (`? ""`, yarn's spelling of a key longer than 1024 + characters). Before, the whole lock was rejected as "not a yarn + lockfile" and no patch in it could be attested (#2052). - **PyPI.** - A requirements line pinned through an environment variable (`six==${SIXV}`) is refused by `vendor` with @@ -583,6 +983,24 @@ a version without a non-empty section in this file. statement. Poetry 2 (`[project]` name, `[tool.poetry]` version) and PDM 0.x (`[tool.pdm]`) layouts are detected instead of failing with `product_undetected` (#642, #1527, #1529, #1547, #1577, #1589, #1653). + - Hosted `rollback` restores a `uv.lock` whose only registry package is + the patched one (a project with one dependency) instead of failing with + "no sibling registry package". The entry is rebuilt on PyPI's default + index with the fields uv writes at the lock's `revision`, and the run + warns `upstream_uv_entry_rebuilt`. It still refuses when the revision is + newer than 3 or uv is configured with a package index (#2052). + - A patched wheel's `Requires-Dist` / `Provides-Extra` headers reach + `uv.lock` only as TOML strings. A header holding a double quote could + add a key to the lock or leave it unparseable while vendored uv + exited 0. A header a lock entry can't hold now refuses the wheel before + any file is written (`pypi_uv_metadata_unsupported`; + `pypi_lock_parse_failed` for a vendored script lock; hosted mode skips + with `python_metadata_unavailable`). That includes the direct references + (`name @ url`) vendored mode used to wire while leaving a lock + `uv lock --check` calls stale (#2052). + - Vendored uv keeps each line's own ending in a `pyproject.toml` that + mixes CRLF and LF, instead of rewriting the file all-LF, and + `vendor --revert` restores it byte for byte (#2052). - **Go.** - In a go 1.16 (unpruned) module where a dependency imports a fork that the root `go.mod` replaces with the patched module, `apply`, @@ -678,6 +1096,11 @@ a version without a non-empty section in this file. failed. `rust-version.workspace = true` is followed to `[workspace.package]`, so vendored mode no longer warns that a standard workspace declares no `rust-version` (#651, #653, #1429). + - `rollback` and `remove` of a hosted Cargo pin no longer panic (exit 101) + on a `Cargo.lock` package whose name is empty or not ASCII, or on a v1 + lock that lists the hosted block twice. A pin with such a block is + refused whole ("is not a crates.io crate name") before any lookup, with + no file changed (#2052). - **JVM.** - A Maven 4 root that builds its subdirectories implicitly (model 4.1.0, `pom` packaging, no ``) is treated as a reactor by hosted @@ -734,6 +1157,43 @@ a version without a non-empty section in this file. counts as drift: `vendor` deletes it and `repair` removes it offline, instead of reporting `already_vendored` while the build fails (#656, #1819). + - A vendored Maven pom resolves what its upstream pom does. When a parent + pom manages siblings at `${project.version}` (log4j up to 2.17, + logback), the suffixed pom resolved them at `-socket.`, + which nothing serves: `vendor` and `vendor --check` reported success and + the build failed. The vendored pom now declares what it inherits at the + base version, between `socket-patch:inherited` markers. `vendor --check` + now fails on a tree 5.0 / 5.1 wrote with that defect when a local Maven + or Gradle cache holds the parent poms, naming the dependency and the + parent, and `vendor` rewrites it. Shapes the pom can't express refuse + with `inherited_version_unresolvable`, and a first vendoring whose + parent poms can't be read with `parent_unavailable`. A parent or BOM + that only a corporate repository holds is read from the local repository + instead of leaving the root unpinned (#2052). + - Vendoring keeps the bytes of a `.mvn/maven.config` that is not UTF-8 (a + Latin-1 `-Duser.name=José`) instead of rewriting them as U+FFFD, and the + revert restores the file instead of leaving the added lines behind + (#2052). + - A Gradle module declared with a computed classifier + (`${osdetector.classifier}`) is refused: by vendored mode with the new + `gradle_classifier_computed`, by hosted mode with + `redirect_gradle_classifier_declared`. Before, `vendor` and + `vendor --check` reported success while Gradle could no longer resolve + the classifier jar (#2052). + - `vendor --revert` finds its `gradle/verification-metadata.xml` entry + after the file was re-serialised + (`./gradlew --write-verification-metadata`) and puts the original entry + back; before, it reported success and left the patched jar's sha256 in + place. When the patched hash sits where no recorded shape applies, + nothing is reverted and `vendor_lock_entry_drifted` names the entry + (#2052). + - In a `settings.gradle.kts`, vendoring creates `pluginManagement {}` + after the file annotations (`@file:Suppress(…)`) instead of above them, + where the script stopped compiling (#2052). + - The Gradle and Maven wrapper version is read from the + `distributionUrl = …` and `distributionUrl: …` spellings too, so a + Gradle 6.7 wrapper no longer passes vendored mode's 6.8 gate and a Maven + 3.9.9 wrapper no longer reads as unknown (#2052). - **NuGet.** - When a `packages.lock.json` locks a patched package at a version the writers refuse to wire (another target framework or @@ -746,6 +1206,9 @@ a version without a non-empty section in this file. `~/.nuget/packages` (#397, #398, #1769), and `-g` runs leave a vendored project's own install alone (`vendored_ownership_retained`) (#489, #1790). + - `rollback` no longer panics on a package id with non-ASCII letters + (`Ünicode.Pkg`) when `nuget.config` maps package sources by prefix + pattern (#2052). - **Composer.** - A `composer.lock` with a UTF-8 BOM is read everywhere, and vendor then `--revert` keeps the BOM byte for byte; a revert no @@ -781,6 +1244,31 @@ a version without a non-empty section in this file. - A vendored revert keeps an artifact the lock still references through an escaped path spelling (`\/` in an older `composer.lock`, Windows backslashes) instead of deleting it (#2022). + - The committed vendor ledger can only undo vendoring's own edits. A + tampered `.socket/vendor/state.json`, which a pull request can carry, + could make `vendor --revert`, `remove`, `rollback`, `scan --prune` or a + takeover write `.git/config`, a CI workflow or any root file, with + exit 0. A record may now name only the files its backend edits, never a + path under `.git` or through a symlink, and may put back only the + pre-vendor form of the package's own entry. A record that doesn't fit + fails that package's revert (exit 1) with nothing written for it, and + the other packages still revert. `nuget.config` snapshots are recorded + without credentials (ones an earlier release recorded are dropped at the + next save but stay in git history, so rotate them), and vendored gems + refuse a `gem` line whose positional argument is computed + (`gemfile_declaration_not_editable`), since it couldn't be reverted + (#2052). + - Finishing an interrupted vendored commit can no longer write under + `.git`, and is never silent. A checkout can carry a hand-written + `.socket/vendor/.commit-journal.json`; one that names a path inside a + git directory, in any spelling a filesystem folds onto `.git`, is now + set aside with nothing applied, like one naming an absolute or `..` + path. A replay prints the files it rolled forward on stderr and adds a + `vendor_commit_recovered` (or `vendor_commit_set_aside`) warning under + `--json`. A `--dry-run` of `apply`, `rollback`, `remove`, `repair` or + `vendor` no longer replays a pending journal: it exits 1 with the new + `vendor_commit_pending` error, where it used to finish the commit and + then preview (#2052). - **General.** - Workspace runs never read or rewrite project files outside the enclosing git repository root. Ancestor workspace roots, settings @@ -861,6 +1349,139 @@ a version without a non-empty section in this file. - Failed upstream registry lookups and downloads now name the transport cause (refused, reset, DNS, TLS, timeout), with URLs redacted. Before, they said only "error sending request" (#1650, #1652). + - A global `scan --prune` / `--sync` (`--global`, `--global-prefix`) keeps + the manifest entries and blobs of the `--cwd` project. Before, it judged + the project's manifest against the global crawl alone, emptied it and + deleted `.socket/blobs/` with exit 0. A pruned manifest that can't be + written now prunes and sweeps nothing (`manifest_write_failed`), and the + blob, diff and package sweeps of `repair`, `remove`, `rollback` and + `scan --prune` refuse a symlinked `.socket/` level (`cleanup_failed`) + instead of deleting the files at the link's target (#2052). + - Hosted mode checks each patch reference the API grants before any of it + is written into project files: a canonical uuid, `https` URLs of plain + URL characters, digests of the right length and alphabet, names and + versions in their ecosystem's grammar. A grant that fails is skipped + with the new `redirect_grant_malformed` (the detail names the field), + and a plain `http` URL anywhere but the `--patch-server-url` origin or a + loopback address with `redirect_grant_insecure_url`; nothing is written + for that package and the exit code is unchanged. Before, a quote, + newline or markup in a grant could add fields to `yarn.lock` or + `Gemfile.lock`, break `pom.xml` or panic the run (#2052). + - Hosted multi-file writes are all or nothing. `rollback`, `remove` and + the withdrawn-pin sweep check every file a restore would write before + touching the first, and put back the files already written when a later + write fails. Before, a symlinked `pom.xml` left `.mvn/maven.config` + deleted and the pin live while `rollback` printed "Restored" and counted + it. Such a pin is never reported restored now: `rollback` emits + `hosted_write_failed` and exits 1, and `--dry-run` previews that failure + with exit 1 where it previewed a success. A hosted `scan` whose write + fails says "(nothing was changed)" only when every file was put back, + and otherwise names each file and the state it is left in (#2052). + - Agent `apply` and `rollback` of a package are all or nothing and keep + file modes. Every file is staged with its final mode and owner before + the first is renamed into place, and a failure puts the files already + replaced back, so a corrupt blob or `EACCES` on a later file no longer + leaves the earlier ones patched, and an interrupt no longer leaves a + `0755` script at `0644`. Errors name the file + (`Cannot apply patch: - `). A killed run can leave + `.socket-stage-*` / `.socket-orig-*` files beside the package's files; + the next `apply` or `rollback` of that package finishes it and removes + them once they are ten minutes old. When a failed write can't put a + file back either, its original is kept beside it as + `.socket-kept--`, which no later run removes (#2052). + - Commands that rewrite `.socket/manifest.json` (`remove`, `rollback`, + `get`, `scan`) keep the top-level and per-record fields this version + doesn't know, such as ones a newer CLI wrote, and the file's mode. + Before, the first rewrite dropped them and a `0600` manifest came back + `0644` (#2052). + - `apply`, `rollback`, `remove` and `repair` set up the API client only + when they have something to download. A run that finds everything on + disk makes no API request, prints no "No SOCKET_API_TOKEN set" notice + and reports no `api_auth_fallback`. A no-op `apply` with a token and no + `--org` used to wait on the organization lookup first: 12 s against an + endpoint that drops packets, 65 s against one that never answers. An + `apply` with a Maven patch in scope still sets the client up (#2052). + - Telemetry opt-outs that did nothing are honoured: `DO_NOT_TRACK` (any + value except `0`, `false` and empty) and + `SOCKET_PATCH_TELEMETRY_DISABLED`, the v3/v4 name that v5 had dropped, + now turn telemetry off like `SOCKET_TELEMETRY_DISABLED`. README and + `docs/configuration.md` gain a Telemetry section listing every event, + field and switch, and the `--no-telemetry` help no longer calls the + events anonymous (#2052). + - A closed output pipe no longer cuts a command short. `apply | head -1` + or `rollback --yes | true` used to die of SIGPIPE (exit 141) at the line + being printed, with `.socket/apply.lock` left behind and, for `rollback` + and `remove`, possibly between the restores and the manifest cleanup. + The command now stops printing, finishes its work, releases the lock and + exits with its own code; a read-only command piped into `head` exits 0 + (#2052). + - Staging entries an interrupted run leaves under `.socket/` are swept: + every lock-taking command except a dry run removes `.socket-stage-*` + entries older than an hour. `apply` now stages the blobs it downloads in + `.socket/.socket-stage-apply-blobs-*`, removed when the run ends, + instead of the system temp directory, so it no longer needs a usable + `$TMPDIR` and a killed run no longer leaves a copy of the blob cache + there (#2052). + - `apply --force` names the patch files it skipped because they are + missing on disk (new `patch_files_skipped` warning), and a re-run that + writes nothing reports `already_patched` instead of an `applied` event + with no files (#2052). + - `apply`, `apply --check` and `list --json` warn + `vendor_state_unreadable` when `.socket/vendor/state.json` can't be + read, instead of silently treating vendored packages as in-place patches + or leaving them out of a `success` listing. Exit codes are unchanged + (#2052). + - `gem env` and the `python3` probes run from `--cwd` for a project and + from the home directory under `--global`, not from wherever the CLI was + started. A version manager's shim (rbenv, pyenv, asdf, mise) therefore + picks the project's interpreter, and the directory the shell is in no + longer steers a global scan (#2052). + - `remove` and `rollback` no longer promise a later restore for a patch + whose package isn't installed. The warning and the + `rollback_not_installed` event now say that nothing was restored, that + the record is gone, and that reinstalling the package is the remedy + (#2052). + - `--help` wraps to the terminal (100 columns at most) instead of running + lines of up to 564 characters, and the root help defines hosted, + vendored and agent mode. The npm launcher explains a missing platform + package (`npm install --omit=optional`) and how to get the binary + instead of printing `spawnSync … ENOENT`, and lists the supported + platforms on an unsupported one (#2052). + - In a project with no patch of any mode, `rollback`, `repair`, `remove` + and `vex` say "No patches in this project; nothing to roll back." (or to + repair, to remove, to attest) instead of "Manifest not found at + ./.socket/manifest.json", a file a hosted project never has. Exit codes + and the `--json` `manifest_not_found` error are unchanged (#2052). + - When `vex` can't detect a product, the error lists what detection reads, + names the project files that lack a usable name and version, and ends + with the run's own flag and a value to paste + (`--product pkg:generic/`, or `--vex-product` on an + embedded run). It still exits 2 with `product_undetected` (#2052). + - `--product` / `--vex-product` and `--doc-id` / `--vex-doc-id` values are + emitted trimmed; surrounding whitespace used to land in the document's + `@id`. A document id with no IRI scheme is honoured with a new + `doc_id_not_iri` warning (#2052). + - A report-only `scan` (`--prune`, `--global`, `--global-prefix`) previews + as the report it is: its `--dry-run` no longer prints "Would download + and apply N patches" for a run that applies nothing, and the listing is + headed `Patches available:` (#2052). + - The hosted summary counts only the packages the run switched and gives + the others a sentence of their own: "Switched 3 packages to hosted + patches; rewrote 2 files. 1 package was already on hosted patches." + `--json` is unchanged (#2052). + - Crafted project files no longer hang, exhaust memory or crash a run: a + workspace glob with many `*`, `**`, brace sets or extglob groups (a + pattern past the bounds fails closed and lists every directory); a Hatch + `matrix` (read up to 1,024 combinations); a chain of Maven `${property}` + references (a value past the bound reads as unresolved); a Gradle string + nested more than 64 templates deep (the script is refused as + unparseable, where `scan` aborted with exit 134); a small crafted + `bun.lockb` that made `vendor` allocate gigabytes; and a torn + `bun.lockb` snapshot in the vendor ledger, which `vendor --revert` now + leaves alone as drift instead of panicking (#2052). + - The `git checkout` remedy printed when a hosted pin can't be restored + names every file hosted mode edited: `Gemfile` with `Gemfile.lock`, and + the `pyproject.toml` or script paired with a `uv.lock` (#2052). ### Performance @@ -874,6 +1495,15 @@ a version without a non-empty section in this file. - Rescans of already-patched bun and yarn berry projects drop repeated per-package lookups that had made them about 20% slower since early October (#1606, #1607, #1693). + - Reverting a vendored pnpm package that much of the lock depends on reads + the lock section once instead of once per dependent. On a 60,000-package + lock a revert took 1 min 51 s against 0.77 s to vendor; a revert over + 20,000 dependents drops from 94 s to about half a second in a debug + build (#2052). + - Vendoring into a large `vlt-lock.json` no longer decodes lock edges for + every comparison. It took 20.9 s for 24 packages on a 20,000-node lock; + planning one package drops from 2.1 s to 12 ms in a debug build + (#2052). - **PyPI.** - Hosted Poetry and PDM scans parse each lock once instead of five or six times (about 31–34% fewer instructions on the bench). VEX @@ -883,12 +1513,26 @@ a version without a non-empty section in this file. - Hosted Go rescans parse `go.sum` once per line instead of re-tokenizing it for every patched dependency, recovering the ~30% slowdown on large `go.sum` files (#1424, #1746). +- **JVM.** + - A Maven pom whose declarations share `${property}` texts is interpolated + once per profile combination instead of once per declaration. A + 1,000-declaration pom that cost 263 million property lookups, 28 s in + `socket-patch list`, now takes under 600,000 units of work (#2052). - **General.** - The project-patch check and purl canonicalization are cheaper on large Yarn Berry locks, about 3% fewer instructions per rescan (#1607, #1701). - Hosted rescans no longer ask the patch API's reference endpoint twice about the same pinned patches (#1606, #1607, #1695). + - Agent `apply` and `rollback` flush the device cache twice per package + instead of twice per file. On a 2,000-file package on macOS, `apply` + drops from 16.4–18.8 s to 1.2–1.6 s and `rollback` from 17.5–18.2 s to + 2.0–2.8 s (#2052). + - Telemetry never delays a command. Events are sent in the background and + the exit waits at most 300 ms for them. With an endpoint that accepts + and never answers, `list` drops from 5.02 s to 0.32 s and prints its + output after 0.02 s instead of 5.02 s. An event still in flight after + the wait is dropped (#2052). ### Maintenance @@ -918,6 +1562,10 @@ a version without a non-empty section in this file. #1734, #1738, #1740, #1742, #1744, #1747, #1760, #1765, #1771, #1775, #1780, #1786, #1797, #1803, #1806, #1814, #1816, #1820, #1828, #1842, #1852, #1854, #1882, #1894, #1895, #1896, #1946, #1961, #2028, #2046. +- Tests for the hosted gem, vendored Maven and pnpm revert fixes: + real-Bundler and real-Maven (log4j-core) capstones, a byte-exact pnpm + revert in the ledger schema e2e, and expectations reconciled across the + merged fixes (#2052). ## [5.0.1] — 2026-10-13