Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,304 changes: 113 additions & 2,191 deletions CHANGELOG.md

Large diffs are not rendered by default.

1,606 changes: 97 additions & 1,509 deletions README.md

Large diffs are not rendered by default.

30 changes: 19 additions & 11 deletions crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# socket-patch CLI contract

This document defines the **public surface** of the `socket-patch` binary. Anything listed here is part of the user-visible contract: third-party scripts, CI pipelines, and the npm/pypi/cargo wrappers depend on it. Changes are governed by the semver policy at the bottom of this file.
This document defines the **public surface** of the `socket-patch` binary. Third-party scripts, CI pipelines, and the npm distribution depend on this contract. Changes are governed by the semver policy at the bottom of this file.

> **Why this exists.** A flag rename, a default-value change, or a JSON key rename can land green and break every shipped wrapper silently. The contract below is backed by the unit tests under `crates/socket-patch-cli/src/**` (`#[cfg(test)] mod tests`) and the parser tests under `crates/socket-patch-cli/tests/cli_parse_*.rs`. Changes that violate the contract must update those tests in lock-step with a major version bump.

For task-oriented guidance, start with [usage](../../docs/usage.md),
[configuration](../../docs/configuration.md), or [v5 migration](../../docs/migrating-to-v5.md).

**Reference:** [Commands](#subcommands) · [Arguments](#global-arguments) ·
[Policy](#socketyml-patch-policy-v50) · [VEX](#manifest-less-vex-lockfile-discovery) ·
[Vendoring](#vendor-command-contract) · [Rollback](#rollback-command-contract-v50) ·
[Environment](#environment-variables) · [JSON](#json-output-shapes) · [Exit codes](#exit-codes)

## Subcommands

| Name | Visible alias(es) | Notes |
Expand All @@ -12,11 +20,11 @@ This document defines the **public surface** of the `socket-patch` binary. Anyth
| `vex` | — | Emit an OpenVEX 0.2.0 attestation derived from the local manifest, the vendor ledger, and the hosted / vendored patch references the project's lockfiles wire (no manifest required; hosted records come from the API) |
| `vendor` | — | Eject patched dependencies into committable `.socket/vendor/` and rewire lockfiles |
| `list` | — | Print patches in the local manifest, plus the vendor ledger's records (v5.0) and the hosted pins the lockfiles wire (labeled; see the action matrix; an empty project exits 0) |
| `get` | `download` | Agent mode by default (`--mode` selects hosted/vendored): fetch + apply a patch; requires positional `identifier` |
| `get` | `download` | Fetch a selected patch in hosted mode by default; `--mode agent` selects in-place application, also the default with `--save-only` or global targeting. Requires positional `identifier`. |
| `apply` | — | Agent mode: apply patches from the local manifest |
| `rollback` | — | **Full-state rollback (v5.0, MAJOR)**: restore original files AND unwind vendored lockfile wiring / restore hosted pins to their upstream registry entries, remove the rolled-back entries from the manifest, and GC their blobs/archives; takes optional variadic positional `targets` (PURL \| UUID \| path glob). See [Rollback command contract](#rollback-command-contract-v50) |
| `remove` | — | Agent mode: remove a patch from manifest (rolls back first); requires positional `identifier` |
| `repair` | `gc` | Agent mode: download missing blobs, re-vendor missing/corrupt vendored artifacts (never re-synthesizing a lost ledger), and clean up unused ones (refuses with `lock_held` when a live process holds the lock; see "Lock lifecycle" below) |
| `remove` | — | Restore and remove one patch across hosted, vendored, and agent state; requires positional `identifier`. |
| `repair` | `gc` | Download missing agent blobs, re-vendor missing/corrupt vendored artifacts (never re-synthesizing a lost ledger), and clean up unused ones (refuses with `lock_held` when a live process holds the lock; see "Lock lifecycle" below) |

Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex` → `vendor`, with `list` to inspect), then the agent-mode (in-place patching) commands.

Expand All @@ -34,7 +42,7 @@ Rows are in `--help` order (v5.0): the hosted/vendored workflow (`scan` → `vex

## Global arguments

In v3.0 every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.
Every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.

| Long | Short | Env var | Default | Type | Semantic |
|---|---|---|---|---|---|
Expand Down Expand Up @@ -180,7 +188,7 @@ The hidden alias `--no-apply` on `get --save-only` is **part of the contract**

### socket.yml patch policy (v5.0)

A repository can **narrow** what `scan` patches with a `patches` block in its root `socket.yml` (the Socket scanner's config file; `version: 2` keeps every other consumer working — they strip or ignore the block). Design record: `docs/design/staged-rollout.md`.
A repository can **narrow** what `scan` patches with a `patches` block in its root `socket.yml` (the Socket scanner's config file; `version: 2` keeps every other consumer working — they strip or ignore the block). Usage guide: [repository patch policy](../../docs/configuration.md#repository-patch-policy).

**Grammar.** Every key is optional; camelCase, like the rest of socket.yml.

Expand Down Expand Up @@ -261,9 +269,9 @@ patches:

### Per-run limit on new patches (`scan --max-new-patches`, v5.0)

`scan --max-new-patches <N|none>` (env `SOCKET_MAX_NEW_PATCHES`; socket.yml `patches.maxNewPatches`) paces a rollout: each run adds at most N patches to packages that had none, the most critical first, and defers the rest to the next run. It applies to `scan` in hosted, vendored and agent mode, wet and `--dry-run`, and to the in-memory engine (napi `maxNewPatches`, `hosted-bundle`); `get` is explicit intent and ignores it. Design: `docs/design/staged-rollout.md` §5.
`scan --max-new-patches <N|none>` (env `SOCKET_MAX_NEW_PATCHES`; socket.yml `patches.maxNewPatches`) paces a rollout: each run adds at most N patches to packages that had none, the most critical first, and defers the rest to the next run. It applies to `scan` in hosted, vendored and agent mode, wet and `--dry-run`, and to the in-memory engine (napi `maxNewPatches`, `hosted-bundle`); `get` is explicit intent and ignores it. Usage guide: [gradual rollout](../../docs/configuration.md#gradual-rollout).

**Classification.** After per-package selection, each selected `(project, purl)` row is compared with the project's **recorded view** — the merged manifest > hosted lockfile pins > vendor ledger that `updates[]` reads (§5.1):
**Classification.** After per-package selection, each selected `(project, purl)` row is compared with the project's **recorded view** — the merged manifest > hosted lockfile pins > vendor ledger that `updates[]` reads:

| Class | Rule | Capped | The writer gets |
|---|---|---|---|
Expand Down Expand Up @@ -422,7 +430,7 @@ hand (the `postinstall`/`dependencies` entries, the `socket-patch[hook]` depende
`socket-patch-bundler` gem are no longer published.

Prefer hosted or vendored mode: their lockfile (and `.socket/vendor/`) edits are the persistence, so
no install step exists. Agent mode (`scan --mode agent`, `get`, `apply`) patches the installed tree
no Socket Patch install hook is needed. Agent mode (`scan --mode agent`, `get --mode agent`, `apply`) patches the installed tree
in place, which the next package-manager install reverts; wire it into CI yourself:

```sh
Expand Down Expand Up @@ -1023,7 +1031,7 @@ State lives at `$XDG_CACHE_HOME`|`~/.cache` (Unix/macOS) or `%LOCALAPPDATA%` (Wi

## Environment variables

All v3.0 env vars use the `SOCKET_*` prefix. Three legacy `SOCKET_PATCH_*` names are still honored at runtime for compatibility: on first read of any of the three the binary emits a one-shot deprecation warning to stderr (the warning fires unconditionally — even under `--silent` / `--json` — because it's a transition signal users need to see). The legacy names will be removed in a future major release.
Public configuration uses the `SOCKET_*` names below. The three deprecated v3/v4 environment aliases were removed in v5; see [Removed env vars](#removed-env-vars).

Four `SOCKET_CLI_*` names from the sibling JS Socket CLI are additionally accepted as **peer aliases** (supported, not deprecated — no warning): `SOCKET_CLI_API_TOKEN` → `SOCKET_API_TOKEN`, `SOCKET_CLI_ORG_SLUG` → `SOCKET_ORG_SLUG`, `SOCKET_CLI_API_BASE_URL` → `SOCKET_API_URL`, `SOCKET_CLI_NO_API_TOKEN` → `SOCKET_NO_API_TOKEN`. The canonical `SOCKET_*` name always wins when both are set; promotion is silent and happens in-process before clap parses. Other socket-cli names (`SOCKET_CLI_CONFIG`, `SOCKET_CLI_API_PROXY`, `SOCKET_CLI_DEBUG`) are deliberately **not** honored.

Expand Down Expand Up @@ -1058,7 +1066,7 @@ Empty string means unset at every layer: exported-but-empty flag-bound vars are
| `SOCKET_NO_NPM_ALLOW_REMOTE_CONFIG` | `--no-npm-allow-remote-config` | `false` | Hosted mode: skip the `allow-remote=all` write to the project `.npmrc`. |
| `SOCKET_NO_VLT_INSTALL_CLEANUP` | `--no-vlt-install-cleanup` | `false` | Hosted mode, `rollback`, `remove`: keep stale vlt installed copies. |
| `SOCKET_FORCE` | `apply --force` / `-f`, `vendor --force` / `-f`, `--update --force` | `false` | Local to `apply`, `vendor` and `--update`. |
| `SOCKET_PATCH_VERSION` | `--update <VERSION>` | (latest) | Local to `--update`; the same pin `install.sh` and the gem launcher honor. |
| `SOCKET_PATCH_VERSION` | `--update <VERSION>` | (latest) | Local to `--update`; the same pin `install.sh` honors. |
| `SOCKET_BATCH_SIZE` | `scan --batch-size` | `500` authenticated / `100` proxy | Local to `scan`. |
| `SOCKET_MAX_NEW_PATCHES` | `scan --max-new-patches` | (unlimited) | Local to `scan` (v5.0): a count or `none`; empty is unset, malformed exits 2. |
| `SOCKET_SCAN_PACKAGES` | `scan --package` | (none) | Local to `scan` (v5.0); comma-separated names or purls. |
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-cli/src/commands/scan/rollout.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Scan's side of the rollout stage (`docs/design/staged-rollout.md` §5,
//! §9.2): `updates[]`, the hosted gate and the human lines. The stage
//! Scan's side of the rollout stage (`docs/configuration.md#gradual-rollout`):
//! `updates[]`, the hosted gate and the human lines. The stage
//! itself is [`socket_patch_core::rollout::stage`].

use std::collections::{BTreeMap, BTreeSet, HashSet};
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-cli/src/commands/scan/rollout_args.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! `scan --max-new-patches` (work item B of the staged-rollout design,
//! `docs/design/staged-rollout.md` §5).
//! `scan --max-new-patches` (see the rollout guide,
//! `docs/configuration.md#gradual-rollout`).


use clap::Args;
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! machinery still has teeth against a tampered pin.
//!
//! The three properties this pins (each validated empirically before the
//! feature was built — see `docs/design/golang-hosted.md`):
//! feature was built — see `docs/ecosystems.md#go-directory-replaces-and-gosum`):
//!
//! 1. **No sumdb consultation**: `GOSUMDB` is set to a bogus database name
//! for every day-2 command. go parses `GOSUMDB` lazily and consults it only
Expand Down
4 changes: 2 additions & 2 deletions crates/socket-patch-cli/tests/e2e_hosted_production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
//! cargo tier emptied on 2026-08-28, so the leg was demoted to the canary
//! (`docs/testing/hosted-production-e2e.md` says how to re-promote it).
//! * **golang** — hosted mode is supported for free-tier references carrying
//! a `goproxy` override (`docs/design/golang-hosted.md`), but production
//! a `goproxy` override (`docs/ecosystems.md#go-directory-replaces-and-gosum`), but production
//! publishes no golang hosted modules yet. Covered as a shape guard that
//! holds in both worlds.
//! * **deno** — hosted mode is not supported. Covered as a negative assertion.
Expand Down Expand Up @@ -2427,7 +2427,7 @@ fn statements_for_opt(doc: Option<&serde_json::Value>, purl: &str) -> usize {
// ===========================================================================

/// Go hosted mode: supported for free-tier references that carry a `goproxy`
/// override (`docs/design/golang-hosted.md`); refused with
/// override (`docs/ecosystems.md#go-directory-replaces-and-gosum`); refused with
/// `redirect_golang_unsupported` otherwise (`golang-hosted-no-go.md`, the
/// paid-tier analysis).
///
Expand Down
6 changes: 3 additions & 3 deletions crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//! `/api/v1/dependencies` fallback returned a zero-byte body); the 2026-08-18
//! gem catalog republish fixed the served index, and this hermetic suite pins
//! the contract from both sides regardless of production's current state —
//! see the history section of `docs/testing/hosted-production-e2e.md`.
//! see `docs/testing/hosted-production-e2e.md` for the live-service counterpart.
//!
//! Unlike the npm/cargo siblings, this suite is FULLY hermetic: the fixture
//! gems are authored here and built with the real `gem build`, and ONE
Expand Down Expand Up @@ -1390,8 +1390,8 @@ async fn gem_hosted_gems_rb_spelling_redirects_and_installs() {

/// The compact-index DEPENDENCY contract, pinned from the red side: a patch
/// registry whose `/info` omits the gem's runtime deps (production's
/// HISTORICAL behavior until the 2026-08-18 republish fixed the served index
/// — see docs/testing/hosted-production-e2e.md's history section) BREAKS the
/// HISTORICAL behavior until the 2026-08-18 republish fixed the served index)
/// BREAKS the
/// prescribed install with bundler's `APIResponseMismatchError`. If the CLI
/// or fixture ever starts tolerating that silently, this turns red.
#[tokio::test(flavor = "multi_thread")]
Expand Down
14 changes: 9 additions & 5 deletions crates/socket-patch-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ Core library for [socket-patch](https://github.com/SocketDev/socket-patch) — a

## What this crate provides

- **Manifest management** — read, write, and validate `.socket/manifest.json` patch manifests
- **Patch engine** — apply and rollback file-level patches using git SHA-256 content hashes
- **Crawlers** — discover installed packages across npm, PyPI, Ruby gems, Cargo, Go, Maven, Composer, NuGet, and Deno
- **API client** — fetch patches from the Socket API
- **Utilities** — PURL parsing, blob storage, hash verification, fuzzy matching
- Dependency discovery from installed packages and lockfiles.
- Hosted dependency rewriting, including the in-memory engine used by the Node bindings.
- Selection policy and gradual rollout shared across disk and in-memory scans.
- Vendored artifact acquisition, verification, wiring, and reversal.
- Agent patch manifests and file-level apply/rollback with content-hash checks.
- Socket API access and OpenVEX generation from patch records and live references.

See the repository's [development guide](../../docs/development.md) for the code map
and [ecosystem matrix](../../docs/ecosystems.md) for supported formats and limits.

## Usage

Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/patch/redirect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5868,7 +5868,7 @@ fn go_token_safe(s: &str) -> bool {
!s.is_empty() && !s.chars().any(|c| c.is_whitespace() || c.is_control())
}

// The committable shape (validated empirically — `docs/design/golang-hosted.md`):
// The committable shape (validated empirically — `docs/ecosystems.md#go-directory-replaces-and-gosum`):
//
// go.mod: replace <orig> <ver> => patch.socket.dev/gopatch/<uuid> <sver>
// go.sum: patch.socket.dev/gopatch/<uuid> <sver> h1:… (zip dirhash)
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/policy/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The repository's patch policy: the `patches` block and
//! `projectIgnorePaths` of the root `socket.yml`, plus the built-in default
//! path ignores. See `docs/design/staged-rollout.md` §3-§4.
//! path ignores. See `docs/configuration.md#repository-patch-policy`.
//!
//! A policy only ever **narrows** what `scan` patches (trust boundary,
//! CLI_CONTRACT.md): nothing here names an endpoint, a credential, a mode
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/rollout/stage.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The per-run rollout stage (`docs/design/staged-rollout.md` §5, §9.2)
//! The per-run rollout stage (`docs/configuration.md#gradual-rollout`)
//! the disk scan and the in-memory engine share: classify the selected
//! offers against the recorded state, spend the budget on NEW packages
//! most critical first once each mode's eligibility checks ran, and
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vendor/go_sum_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! ```
//!
//! Both lines are load-bearing on day-2 machines (validated empirically —
//! see `docs/design/golang-hosted.md`): under the default `-mod=readonly` a
//! see `docs/ecosystems.md#go-directory-replaces-and-gosum`): under the default `-mod=readonly` a
//! missing zip line fails resolution up front, a missing `/go.mod` line fails
//! after download, and a *present* line is verified against the fetched bytes
//! (a wrong hash is a hard `SECURITY ERROR`). Crucially, go consults the
Expand Down
2 changes: 1 addition & 1 deletion crates/socket-patch-core/src/vendor/golang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,7 @@ mod tests {
.any(|e| e.module == MODULE && e.owner == Some(ReplaceOwner::Vendor)));
}

/// Cross-mode policy regression (docs/design/golang-hosted.md): vendor
/// Cross-mode policy regression (docs/ecosystems.md#go-directory-replaces-and-gosum): vendor
/// takes over a hosted-mode replace through the LOCAL build leg too — only
/// local *apply* refuses a Hosted-owned directive (its go-patches copy is
/// uncommitted, so the takeover would break other machines). The hosted
Expand Down
Loading
Loading