-
Notifications
You must be signed in to change notification settings - Fork 0
Comparing changes
Open a pull request
base repository: SocketDev/socket-patch
base: 3a58178
head repository: SocketDev/socket-patch
compare: c20619b
- 13 commits
- 192 files changed
- 3 contributors
Commits on Apr 9, 2026
-
fix: use npx/pnpm dlx @socketsecurity/socket-patch and add dependenci…
…es script (#63) Update the setup command to generate the correct npx/pnpm dlx command prefix based on lockfile detection, and configure both postinstall and dependencies lifecycle scripts. - Add PackageManager enum (Npm/Pnpm) with lockfile detection - Generate `npx @socketsecurity/socket-patch apply` for npm projects - Generate `pnpm dlx @socketsecurity/socket-patch apply` for pnpm projects - Add dependencies lifecycle script alongside postinstall - Thread PackageManager through detect -> update -> setup pipeline Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for d2cf981 - Browse repository at this point
Copy the full SHA d2cf981View commit details
Commits on May 12, 2026
-
Bump quinn-proto from 0.11.13 to 0.11.14 (#40)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.13 to 0.11.14. - [Release notes](https://github.com/quinn-rs/quinn/releases) - [Commits](quinn-rs/quinn@quinn-proto-0.11.13...quinn-proto-0.11.14) --- updated-dependencies: - dependency-name: quinn-proto dependency-version: 0.11.14 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for d9ff0e5 - Browse repository at this point
Copy the full SHA d9ff0e5View commit details -
chore(deps): Bump rand from 0.9.2 to 0.9.4 (#65)
Bumps [rand](https://github.com/rust-random/rand) from 0.9.2 to 0.9.4. - [Release notes](https://github.com/rust-random/rand/releases) - [Changelog](https://github.com/rust-random/rand/blob/0.9.4/CHANGELOG.md) - [Commits](rust-random/rand@rand_core-0.9.2...0.9.4) --- updated-dependencies: - dependency-name: rand dependency-version: 0.9.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for ec5d349 - Browse repository at this point
Copy the full SHA ec5d349View commit details -
chore(deps): Bump rustls-webpki from 0.103.9 to 0.103.13 (#66)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.13. - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](rustls/webpki@v/0.103.9...v/0.103.13) --- updated-dependencies: - dependency-name: rustls-webpki dependency-version: 0.103.13 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 5550100 - Browse repository at this point
Copy the full SHA 5550100View commit details
Commits on May 19, 2026
-
feat(patch): add package- and diff-level patch sources (#67)
* feat(patch): add package- and diff-level patch sources Adds two new optional pathways to the socket-patch CLI alongside the existing per-file blob path: - Per-package archives at `.socket/packages/<uuid>.tar.gz` — a tarball of patched files for a single patch, extracted in one shot. - Per-file bsdiff archives at `.socket/diffs/<uuid>.tar.gz` — bsdiff deltas that transform `before_hash` content into `after_hash` content. The apply pipeline now tries sources in the order package → diff → blob, falling through to the next on any failure. Every strategy post-write-verifies the file's git-sha256 against `after_hash`, so the existing safety invariant is unchanged. A new `--download-mode {diff,package,file}` flag (default: `diff`) controls what `apply`, `get`, `scan`, and `repair` fetch when local artifacts are missing. The manifest schema is intentionally unchanged: archives are keyed by patch UUID (already present in `PatchRecord`), so legacy manifests keep working with no migration. Highlights: - New core modules `patch/diff.rs` (qbsdiff bspatch wrapper) and `patch/package.rs` (tar+flate2 reader with path-traversal guards, whitelist filtering against `expected_files`, and hard caps on decompressed bytes / per-entry size / entry count to defuse gzip-bomb and `Vec::with_capacity` allocation attacks). - New `PatchSources` struct and `AppliedVia` enum in `patch/apply.rs`; `apply_package_patch` takes a `PatchSources` and an optional UUID. Passing `uuid = None` restores pre-2.2 blob-only behavior. - `try_apply_from_diff` gates on the captured pre-apply `current_hash` rather than `VerifyStatus`, so `--force` cannot drive a diff against garbage content. - `apply`'s offline guard now reports per-patch source availability instead of a global blobs/diffs/packages bucket count. - `ApiClient::fetch_diff(uuid)` and `fetch_package(uuid)` mirror `fetch_blob(hash)`; a private `fetch_binary` helper deduplicates the proxy/auth client split and 200/404/error handling. - `DownloadMode` enum + `fetch_missing_sources` in `api/blob_fetcher.rs` dispatch downloads by kind. `cleanup_unused_archives` in `utils/cleanup_blobs.rs` reaps orphaned `.socket/packages/` and `.socket/diffs/` files via `repair`. Tests: 307 unit + 2 e2e gem (was 263 + 2 before this change). New coverage spans diff round-trips, package extraction safety (traversal, oversize-header, too-many-entries, decompression-bomb truncation), fallback chain ordering (`via package`/`diff`/`blob`), force-mode + diff regression, dry-run safety, UUID validation, and archive download/cleanup helpers. All existing tests pass unchanged. Server-side `/patch/diff/<uuid>` and `/patch/package/<uuid>` endpoints are not live yet — 404 responses fall through gracefully to the file blob path, so this PR ships safely ahead of server support. Assisted-by: Claude Code:claude-opus-4-7 * chore: clean up stray dead-code markers - e2e_npm.rs: NPM_PURL is actually used by 5 assertions; drop the stale `#[allow(dead_code)]`. - maven_crawler.rs: remove `read_pom_in_dir`, an async helper that was never called and only existed under `#[allow(dead_code)]`. No behavior change. 307 tests still pass; cargo build clean. Assisted-by: Claude Code:opus-4-7Configuration menu - View commit details
-
Copy full SHA for 6288a37 - Browse repository at this point
Copy the full SHA 6288a37View commit details
Commits on May 20, 2026
-
test(cli): foundation for CLI-contract unit-test campaign (#68)
* test(cli): comprehensive unit-test campaign for the CLI contract Add unit-test coverage for every subcommand, flag, default, alias, and helper in the socket-patch CLI, plus a new CLI_CONTRACT.md that documents the surface as semver-significant. Before this change the CLI crate had zero unit tests under src/ — only network-dependent tests/e2e_*.rs suites gated on --ignored. A flag rename, a default change, or a JSON key drift could land green and break every shipped wrapper (npm @socketsecurity/socket-patch, pypi socket-patch, cargo, prebuilt binaries). ## What's covered Library surface (new src/lib.rs): - Cli, Commands, looks_like_uuid, parse_with_uuid_fallback extracted from main.rs so integration tests can verify the parser without spawning the binary. - main.rs becomes a thin wrapper that delegates to the lib. - Cargo.toml gains [lib] alongside [[bin]]. Core helper extraction: - socket_patch_core::manifest::operations::resolve_manifest_path replaces the 5-line absolute-vs-relative join block previously copy-pasted into apply/rollback/list/remove/repair (5 callers). CLI_CONTRACT.md (new): - Documents every subcommand, flag, default value, visible alias (download, gc), hidden alias (--no-apply), JSON output shape, and exit code as semver-significant. - Pins the divergent defaults: --download-mode=diff for apply/get/scan and --download-mode=file for repair, --batch-size=100 for scan. - Spells out the bump policy and how to invoke scripts/version-sync.sh. Helper unit tests (#[cfg(test)] mod tests in each file): - src/lib.rs — looks_like_uuid (valid/invalid shapes, case-insensitive), parse_with_uuid_fallback (success, fallback, fallback-fails preserves original error, no double-rewrite). - src/output.rs — format_severity, color, confirm (skip_prompt and is_json short-circuits; interactive path intentionally not tested). - src/ecosystem_dispatch.rs — partition_purls (filter, dedup, unknown ecosystem dropped). - commands/apply.rs — verify_status_str (all 4 VerifyStatus variants), result_to_json (top-level + filesVerified key sets). - commands/rollback.rs — find_patches_to_rollback (None=all, PURL match, UUID match, no-match=empty). - commands/get.rs — detect_identifier_type (UUID/CVE/GHSA/PURL/None, case-insensitive for CVE+GHSA), select_patches (paid auto, free single auto, free multi+is_json -> Err(1)). Clap parser snapshot tests (new tests/cli_parse_*.rs): - One file per command: apply, get, list, main, remove, repair, rollback, scan, setup. - Every flag (long + short) has at least one parser assertion. - Every #[arg(default_value)] is asserted on the no-flag parse. - The download visible alias on get is exercised. - The gc visible alias on repair is exercised. - The hidden --no-apply alias on get --save-only is exercised. - --ecosystems CSV splitting is verified on apply/rollback/scan. - The bare-UUID rewrite is exercised end-to-end via Cli::try_parse_from. - Failure paths assert clap::error::ErrorKind variants. Async run() integration tests: - tests/cli_parse_list.rs covers missing manifest -> 1, empty -> 0, populated -> 0, absolute-path override, and a subprocess JSON-shape assertion against the compiled binary. - tests/cli_parse_remove.rs covers missing-manifest -> 1. - tests/cli_parse_setup.rs covers no-package-json -> 0 with the JSON status:"no_files" shape pinned via subprocess. ## Verification cargo build --workspace --all-features cargo clippy --workspace --all-features -- -D warnings cargo test --workspace --all-features All clean. 79 new lib tests + 156 new integration tests added on top of the existing 415 unit tests; cumulative 650 tests pass. ## Why squashed Originally landed as a foundation PR (#68) plus 10 sibling test PRs (#69-#78), one per command/file, dispatched in parallel. Squashing the sibling PRs back into #68 so the contract + tests land as one self-contained unit — reviewers see the full picture, and a future revert touches one commit instead of eleven. Assisted-by: Claude Code:claude-opus-4-7 * ci: tighten CI matrix — add macOS, release-mode tests, explicit build step The CI workflow's unit-test job ran on ubuntu-latest + windows-latest; extend the matrix with macos-latest so the new CLI parser tests are exercised on every platform the binary ships for. socket-patch ships prebuilt binaries for x86_64-apple-darwin and aarch64-apple-darwin (see release.yml), so silent macOS-specific regressions in path handling, TTY detection, or terminal escapes are real risks today. Three changes: - Add `macos-latest` to the test matrix. - Add `fail-fast: false` so a failure on one OS doesn't mask failures on the others. - Add an explicit `cargo build --workspace --all-features` step before `cargo test`. `cargo test` already builds, but a dedicated build step gives a cleaner red signal when a build-only failure happens (e.g. a feature-gated compile error) without the noise of test-discovery output. - New `test-release` job: `cargo test --workspace --all-features --release` on ubuntu-latest. Catches optimization-level regressions that debug mode hides (e.g. release-mode-only inlining changes that affect assertion behavior). One OS keeps total CI time reasonable while still locking in release-mode correctness. Assisted-by: Claude Code:claude-opus-4-7 * fix(patch): reject POSIX-style absolute paths in archive entries on Windows `read_archive_to_map` rejects entries whose path is absolute or contains a `..` component, but the check used `Path::is_absolute()` alone. On Windows that function requires a drive letter or UNC prefix, so a tar entry like `/etc/passwd` is NOT considered absolute and would slip through the guard — when later joined to the target directory, Windows would treat it as relative to the current drive's root. Add an explicit check for a leading `/` or `\` byte alongside `Path::is_absolute()` so the guard rejects POSIX-style absolute paths on every platform. The new test_read_archive_rejects_backslash_absolute_paths case locks the symmetric backslash form in. This was uncovered when the CI matrix was extended to actually run on Windows. The existing test_read_archive_rejects_absolute_paths failed on windows-latest because it constructed the archive with a POSIX-style path that the platform-specific `is_absolute()` did not catch. Assisted-by: Claude Code:claude-opus-4-7Configuration menu - View commit details
-
Copy full SHA for b96a13f - Browse repository at this point
Copy the full SHA b96a13fView commit details
Commits on May 22, 2026
-
feat(scan): unified auto-update engine — --sync, --prune, --dry-run (…
…v3.0) (#79) * feat(scan): add --apply + structured updates for auto-update bot workflows Enables `socket-patch scan` as the engine for an automated "update all patches" workflow — a cron job or PR check that runs scan, detects new or updated patches against the local manifest, applies them, and either commits the change or opens a PR. Today this isn't quite possible because: * `scan --json` is read-only — it prints the discovery JSON and exits before the apply path runs, so there's no clean way to make it mutate the manifest from a bot. * Updates aren't reported in JSON — update detection (existing manifest entry with same PURL but different UUID) only runs in the non-JSON table-print path, so a `--json` consumer can't tell which patches would be updates vs net-new additions. * Per-patch JSON records lose the added-vs-updated distinction — every successful download is reported as `action: "added"` even when it's replacing an existing entry with a newer UUID. Three additive (semver-MINOR) changes resolve all of the above: 1. `commands/get.rs` — `download_and_apply_patches` now emits per-patch `{action: "updated", oldUuid}` when the PURL already had a different UUID before insert. A new pure helper `decide_patch_action(manifest, purl, new_uuid)` returns `Added | Updated{old_uuid} | Skipped` and is unit-tested independently. 2. `commands/scan.rs` — new `--apply` flag (default `false`) opts JSON callers into the full discover → select → apply pipeline. Without `--apply`, `scan --json` keeps its prior read-only contract; with it, `scan --json --apply` runs the same selection + download path the non-JSON branch uses and emits one combined JSON object with an `apply` sub-object reporting per-patch outcomes. The JSON discovery emission also now always includes a top-level `updates` array (with `purl`, `oldUuid`, `newUuid`) computed via a new pure helper `detect_updates`. `severity_order` is exposed as `pub(crate)` so it can be unit-tested. 3. `CLI_CONTRACT.md` documents the new `--apply` flag, the full `scan` discovery and `--apply` JSON shapes, and pins the per-patch action vocabulary (`added`/`updated`/`skipped`/`failed`) with semver policy clauses for adding (MINOR) or renaming/removing (MAJOR) values. ## Tests * scan.rs inline #[cfg(test)] mod tests — 4 severity_order cases + 8 detect_updates cases covering: no manifest, empty packages, no overlap, same UUID, different UUID, multiple updates, empty patch list, first-patch candidate selection. * get.rs inline test module — 4 decide_patch_action cases covering Added (no existing entry), Skipped (same UUID), Updated (different UUID with oldUuid populated), and Added-for-different-PURL (keying on PURL not UUID). * tests/cli_parse_scan.rs — `--apply` parser tests (defaults false, long form, combines with --json/--yes) + a subprocess JSON-shape test that runs the compiled binary against an empty tempdir and asserts the new `updates: []` key is present in stdout. All 416 lib tests pass, all integration tests pass, clippy clean. ## How a bot uses this ```bash socket-patch scan --json --apply --yes > scan-result.json jq '.apply.patches[] | select(.action == "updated") | {purl, oldUuid, uuid}' scan-result.json # Pipe into peter-evans/create-pull-request with a PR body summarizing the diff. ``` Exit code: 0 on full success (every selected patch added/updated/skipped), 1 if any `failed` records are present (and top-level `status` becomes `"partial_failure"`). Assisted-by: Claude Code:claude-opus-4-7 * feat(scan)!: garbage-collect on scan + hide gc subcommand (v3.0) After PR #79's --apply work, scan applied patches but didn't reconcile state. Orphan blob files accumulated and manifest entries for uninstalled packages stayed forever, forcing bots to chain `scan --apply` with `repair` themselves. This commit makes scan the single command needed for the auto-update workflow: * Default GC after every scan run that has scanned packages. Removes manifest entries for PURLs no longer in the crawl results, then sweeps orphan blob/diff/package-archive files via the existing cleanup_unused_blobs / cleanup_unused_archives helpers. * New --no-prune flag opts OUT of GC entirely. Useful when a missing package reflects a temporary uninstall the user wants to preserve. * The `gc` subcommand alias (and `repair` itself) is hidden from socket-patch --help. `socket-patch gc` still parses for backwards compat, just no longer listed. Existing scripts unaffected. * Workspace version bumped 2.1.4 → 3.0.0. scripts/version-sync.sh propagated the bump to every npm/socket-patch-* package.json and to pypi/socket-patch/pyproject.toml. ## JSON output additions In `scan --json` (read-only): new `gc` sub-object reports what *would* be pruned/reaped without mutating anything (preview mode). Fields: prunableManifestEntries, orphanBlobs, orphanDiffArchives, orphanPackageArchives, bytesReclaimable. In `scan --json --apply`: `gc` switches to mutation mode. Fields: prunedManifestEntries, removedBlobs, removedDiffArchives, removedPackageArchives, bytesFreed. With --no-prune: gc is emitted as { "skipped": true } in both modes. In the empty-crawl case (no packages found at all), gc is { "skipped": true } — pruning every manifest entry on the assumption the user "uninstalled everything" is too destructive. ## Tests * 5 new detect_prunable unit tests covering empty manifest, all present, missing entries, and full prune. * --no-prune parser tests in tests/cli_parse_scan.rs (default false, long form, combines with --apply/--json/--yes). * 4 new tests in tests/cli_parse_repair.rs locking the v3.0 deprecation: top-level --help doesn't list `repair` or `[aliases: gc]`, but `socket-patch gc` still resolves to Repair and `socket-patch repair --help` still works directly. * CleanupResult gains #[derive(Default)] so scan can build empty summaries when the cleanup helpers report errors. cargo build/clippy/test --workspace --all-features all clean. 100 lib tests in CLI (+5), 19 in cli_parse_repair (+4), 26 in cli_parse_scan (+2). 416 lib tests in core unchanged. ## Breaking changes (MAJOR bump 2.1.4 → 3.0.0) * scan --apply prunes manifest entries for uninstalled packages by default. Scripts that ran `scan --apply --yes` and relied on manifest entries surviving across an uninstall break unless they add --no-prune. * scan --apply removes orphan blob/archive files on every run (non-breaking in practice — the apply path simply re-fetches anything it needs — but a visible filesystem change). * `socket-patch gc` no longer appears in top-level --help. The subcommand still works. Assisted-by: Claude Code:claude-opus-4-7 * test(scan): add e2e_scan test suite + CI matrix entry End-to-end tests for the scan + GC pipeline that uses the real Socket API. Mirrors the structure of tests/e2e_npm.rs — every test is #[ignore] so it only runs with --ignored, matching the existing e2e gating in .github/workflows/ci.yml. Uses the minimist@1.2.2 patch fixture (CVE-2021-44906) that the other e2e tests already share. ## Coverage (9 scenarios) * test_scan_apply_json_adds_new_patch — fresh install, `scan --json --apply --yes` reports action: "added" and patches the file on disk. * test_scan_apply_json_skips_existing — re-run shows action: "skipped". * test_scan_apply_json_updates_existing — seed manifest with a fake UUID, re-run shows action: "updated" with oldUuid populated. * test_scan_json_read_only_emits_updates_array — read-only mode surfaces the manifest-vs-API drift in the `updates` array. * test_scan_json_read_only_no_mutation — `scan --json` never creates a manifest or modifies files. * test_scan_apply_prunes_uninstalled_package_by_default — uninstall minimist, re-scan, manifest entry is gone + blobs are reaped. * test_scan_apply_no_prune_keeps_uninstalled_entries — same scenario with --no-prune leaves manifest + blobs intact, gc reports { skipped: true }. * test_scan_apply_cleans_orphan_blobs — plant a stray orphan blob, next scan run removes it and reports gc.removedBlobs >= 1. * test_scan_json_read_only_gc_preview — preview mode lists prunableManifestEntries and counts orphanBlobs without mutating. ## CI integration * Added e2e_scan to the e2e job matrix on ubuntu-latest and macos-latest (mirrors how e2e_npm is matrixed). * Setup Node.js step's `if:` predicate extended to also run for e2e_scan — the suite shells out to `npm install` for fixture setup. Each #[ignore] test self-skips with a SKIP message if `npm` is not on PATH, so a future runner without npm doesn't fail spuriously. Assisted-by: Claude Code:claude-opus-4-7 * docs(scan): document v3.0 GC behavior + repair deprecation CLI_CONTRACT.md changes: * Add --no-prune row to the scan flag table with a description of the v3.0 GC default. * Extend the scan JSON output shape with the new `gc` sub-object. Document the split between preview-mode field names (prunable*/orphan*/bytesReclaimable) and apply-mode field names (pruned*/removed*/bytesFreed). Document that --no-prune emits gc: { skipped: true } in both modes. * Mark `repair` as "(deprecated since v3.0)" at the section heading. Spell out the demotion: `hide = true` on the Repair variant and `alias = "gc"` (was `visible_alias`). Removing repair or unhiding it would be a MAJOR bump. * Add semver-policy row: "Change `scan`'s default behavior (e.g. pruning, GC, apply) — MAJOR." Notes the v3.0 flip is the one grandfathered instance; future flips also MAJOR. README.md changes: * Remove the `repair`/`gc` section from the public command list (still documented in CLI_CONTRACT.md for advanced users). * Expand the `scan` section: add --apply and --no-prune flags, -y/--yes, --download-mode rows. New "Bot mode" example with `scan --json --apply --yes`. Add "Apply without pruning" example. Brief note about scan being the single command for the auto-update workflow. Assisted-by: Claude Code:claude-opus-4-7 * fix(scan): auto-select in JSON --apply when multiple free patches exist The free-tier patch API may serve multiple free patches for the same PURL (e.g., minimist@1.2.2 currently has 2 free patches). The `scan --json --apply` path was calling `select_patches(... is_json = true)` which returns `Err(JsonModeNeedsExplicit)` with `status: "selection_required"` in that scenario — no forward progress, the bot can't apply anything. For scan-driven workflows there's no "specify --id" option (we're scanning the whole project), so the right behavior is to auto-select the newest patch and continue. Pass `is_json = false` so the non-TTY branch inside `select_one` auto-selects index 0 — which is the most-recently-published patch (the group is sorted by `published_at` descending before `select_one` runs). Also relaxed the e2e_scan test assertions so they don't pin a specific upstream UUID/hash: * added/updated/skipped tests assert action vocabulary, PURL match, and "file was patched" (not exact AFTER_HASH). * updated test asserts the new UUID differs from the seeded oldUuid rather than matching a hardcoded constant. * read-only updates test similarly asserts `newUuid != oldUuid`. These changes make the e2e suite robust to API churn — the contract is "an apply happened", not "this specific patch was selected". Assisted-by: Claude Code:claude-opus-4-7 * fix(scan): run apply-mode GC even when no packages have patches The post-uninstall scenario hit an edge case: after a user uninstalls the only patched package and installs a new (unpatched) one, the next `scan --apply --yes` would crawl successfully, find no packages with patches, and skip the entire `--apply` block. The read-only preview GC ran instead, emitting `gc.prunableManifestEntries` (preview field name) but never actually pruning anything from the manifest. A bot relying on `scan --apply` to reach a clean state would loop forever — the stale manifest entry never gets removed. Fix: when `--apply` is set but no packages have patches, still run the mutating GC pass and emit an empty `apply` sub-object plus the `gc.prunedManifestEntries` (apply field name). Bots can now trust `scan --apply --yes` to converge to a clean state in one pass even when the crawl has no patched packages. Also dropped the now-unused `NPM_UUID` and `AFTER_HASH` constants from the e2e_scan test file (warning noise from relaxing the assertions in the previous commit). Assisted-by: Claude Code:claude-opus-4-7 * feat(scan): pivot GC to opt-in via --prune + add --sync and --dry-run Reverses the v3.0 GC-by-default decision. After feedback, default-on GC was too aggressive: scripts running `scan --apply --yes` against a project with a temporarily-uninstalled package would silently destroy the manifest entry, breaking dev workflows where the package gets reinstalled later. The new opt-in model: * `--prune` (new, default false) opts into garbage collection. Manifest entries for packages no longer in the crawl are removed, then `cleanup_unused_blobs` + `cleanup_unused_archives` sweep orphan files. Without `--prune`, scan leaves `.socket/` alone. * `--sync` (new) is sugar for `--apply --prune`. The canonical bot invocation becomes `scan --json --sync --yes` (3 flags; `--json` and `--yes` are workflow scaffolding). * `--dry-run` / `-d` (new) previews what `--apply`/`--prune`/`--sync` would do without mutating disk. The `apply.patches[*]` array is populated via `decide_patch_action`, and `gc.prunable*` / `gc.orphan*` field names are emitted (instead of `pruned*` / `removed*`). The `apply.dryRun: true` flag explicitly marks the output for bots that need a single signal. * `--no-prune` field removed (it was the inverse of the now-default behavior). ## Implementation * `ScanArgs.no_prune` → `ScanArgs.prune` (semantics inverted). New `sync` and `dry_run` fields. * At the top of `scan::run`, `let apply = args.apply || args.sync;` and `let prune = args.prune || args.sync;` — derive once, use everywhere downstream. `--sync` is purely additive sugar. * `run_apply_gc` no longer takes a `no_prune: bool` parameter — callers always gate on `prune` before calling it. When GC isn't requested, the `gc` JSON field is omitted entirely (no `{ "skipped": true }` placeholder). * New `preview_apply_gc` helper for the dry-run path. Runs `cleanup_unused_blobs` / `cleanup_unused_archives` with `dry_run=true` and emits preview field names via `GcSummary::to_preview_json`. * Dry-run apply path synthesizes per-patch `apply.patches[]` records via `super::get::decide_patch_action` against the on-disk manifest — accurately reports added/updated/skipped for the selected patches without actually calling `download_and_apply_patches`. * Empty-cwd JSON branch drops `gc: { skipped: true }` (no `gc` field at all when GC wasn't requested). Drive-by fix: `tests/ecosystem_dispatch::partition_purls_allow_list_excludes_one` now uses `!map.contains_key(&Ecosystem::Pypi)` instead of the `unnecessary_get_then_check` lint trigger. Assisted-by: Claude Code:claude-opus-4-7 * revert(repair): restore gc as a documented subcommand The prior v3.0 iteration demoted `Commands::Repair`'s `gc` alias to a hidden `alias = "gc"` and added `hide = true` on the subcommand itself, banking on `scan` becoming the all-in-one command for both apply and GC. With GC now opt-in via `--prune`/`--sync` (see prior commit), `repair`/`gc` is the right answer for users who want to clean up without an apply pass. Restore `#[command(visible_alias = "gc")]` and drop `hide = true` so the subcommand appears in `socket-patch --help` again with its `[aliases: gc]` hint. Update the four hidden-help tests in `tests/cli_parse_repair.rs`: * `repair_is_hidden_from_top_level_help` → `repair_appears_in_top_level_help` (assertion inverted). * `gc_alias_is_hidden_from_top_level_help` → `gc_alias_is_visible_in_top_level_help` (assertion inverted). * `gc_alias_still_parses_for_backwards_compat` → `gc_alias_parses_as_repair` (simplified — alias is no longer deprecated, so the "backwards compat" framing is gone). * `repair_subcommand_help_still_works_directly` dropped (was a deprecation-era assertion). These tests now lock the *opposite* contract: removing or hiding the `gc` alias is a MAJOR bump. Assisted-by: Claude Code:claude-opus-4-7 * test(scan): update parser + e2e tests for opt-in GC cli_parse_scan.rs: * defaults_match_contract now asserts !args.prune, !args.sync, !args.dry_run (replacing the old !args.no_prune line). * no_prune_flag_long_form → prune_flag_long_form; assertion inverted (passing --prune sets prune=true). * no_prune_combines_with_apply_and_json → prune_combines_with_apply_and_json. * NEW: sync_flag_long_form — --sync sets sync=true; does NOT auto-derive --apply/--prune at parse time (that derivation happens inside scan::run). * NEW: sync_combines_with_json_and_yes. * NEW: dry_run_long_form (--dry-run sets dry_run=true). * NEW: dry_run_short_form (-d sets dry_run=true). e2e_scan.rs: * Module docstring updated to describe opt-in GC. * test_scan_apply_prunes_uninstalled_package_by_default → test_scan_apply_prune_prunes_uninstalled_package — now passes --prune explicitly. * test_scan_apply_no_prune_keeps_uninstalled_entries → test_scan_apply_default_keeps_uninstalled_entries — drops the --no-prune flag (it no longer exists); asserts the gc field is omitted entirely. * test_scan_apply_cleans_orphan_blobs → test_scan_apply_prune_cleans_orphan_blobs — passes --prune. * test_scan_json_read_only_gc_preview split into: - test_scan_dry_run_sync_previews_apply_and_gc — exercises the new --dry-run flag combined with --sync; verifies preview output is populated AND nothing on disk changed. - test_scan_json_no_gc_field_without_prune — locks the contract that `gc` is omitted when --prune isn't set. * NEW: test_scan_sync_yes_full_lifecycle — installs minimist, runs --sync (adds patch), uninstalls + plants orphan, runs --sync again (prunes + sweeps). End-to-end exercise of the canonical bot mode. Total e2e_scan scenarios: 11 (was 9). Assisted-by: Claude Code:claude-opus-4-7 * docs(scan): document --prune/--sync/--dry-run + un-deprecate gc CLI_CONTRACT.md: * Scan flag table: replace --no-prune row with three new rows — --prune, --sync, -d/--dry-run. Add a paragraph explaining each plus the canonical bot-mode invocation. * JSON output shape: drop the --no-prune-emits-{skipped:true} note. Clarify that `gc` is omitted ENTIRELY when --prune/--sync isn't set. Document --dry-run behavior including the explicit `apply.dryRun: true` marker for bots. * New "scan — --sync (bot mode)" section with the canonical `scan --json --sync --yes | jq '{applied, pruned, bytes_freed}'` recipe. * New "scan — --dry-run" section explaining that --dry-run is a no-op without one of the mutating flags. * Restore the `repair` section's normal heading (drop the "*(deprecated since v3.0)*" suffix and the deprecation paragraph). Note that the `gc` visible_alias is now contract-guarded. * Semver-policy table: drop the GC-default row, add explicit rows for "flipping --prune to opt-out" and "demoting `gc` from visible_alias" — both MAJOR. README.md: * Restore the `### repair` / `gc` section that was removed during the deprecation iteration. Wording clarifies that `repair`/`gc` is the right answer for cleanup-without-apply and points users at `scan --sync` for the combined workflow. * `### scan` section: replace --no-prune row with --prune, --sync, --dry-run, --yes. Bot-mode example becomes `scan --json --sync --yes` (the user's "one or two flags" target). Add a `scan --json --sync --yes --dry-run` example. * `## Scripting & CI/CD`: lead with the new `--sync` recipe piped through jq into `peter-evans/create-pull-request`. Keep the old `scan --json --ecosystems npm` read-only example as the second use case. Assisted-by: Claude Code:claude-opus-4-7 * chore(hardening): pin toolchain, deps, actions, and install bootstrap - rust-toolchain.toml: pin channel to exact version with components - Cargo.toml: exact-pin all workspace dependencies via =X.Y.Z spec - crates/{cli,core}/Cargo.toml: wire dev-deps through workspace pins - npm/socket-patch/package.json: exact-pin runtime + dev deps; commit package-lock.json so downstream installs are deterministic - scripts/install.sh: download SHA256SUMS, verify tarball digest before extraction; accept SOCKET_PATCH_VERSION env override - scripts/version-sync.sh: preserve the leading = on exact-pin specs; refresh the npm lockfile on every version bump - .github/workflows/release.yml: SHA-pin actions, pin npm@version, pin language toolchain versions for setup-* actions - .github/workflows/pin-check.yml: new fail-closed workflow that greps every uses: line and rejects non-SHA-pinned action refs * feat(cli)!: non-mutating apply + unified JSON envelope (v3.0) Two related changes that together complete the v3.0 contract: * apply no longer writes to .socket/. When the manifest is missing blobs in offline mode, apply bails with a partial_failure envelope. When online and missing blobs need fetching, the bytes go to an OS tempdir overlay for the duration of the run; .socket/ stays read- only. Garbage collection moves out of apply entirely (now lives in scan --prune / repair / gc). * New crates/socket-patch-cli/src/json_envelope.rs defines a shared Envelope/PatchEvent/Status/Summary shape that every --json invocation now emits. The action vocabulary (added/updated/skipped/ applied/downloaded/removed/failed/verified) is the single contract downstream consumers route on. CLI_CONTRACT.md is updated with the unified shape + jq recipes. Migrated commands: apply, list, repair, remove. (scan, get, rollback, setup retain their pre-v3.0 shapes for now and are documented as pending in CLI_CONTRACT.md.) Also removes three dead-code items the audit confirmed have zero callers: - crates/socket-patch-core/src/utils/enumerate.rs (whole module) - crates/socket-patch-core/src/utils/global_packages.rs (whole module; npm crawler ships the live copy) - path_to_group_id() in maven_crawler (test-only inverse helper) - false-positive allow(dead_code) on get.rs::DownloadParams BREAKING: every migrated subcommand's --json output is reshaped to the new envelope (camelCase status, events array, summary block). * test: comprehensive in-process + subprocess test suite Adds ~120 new tests across the apply/scan/get/list/remove/repair/ rollback/setup CLI commands. Tests drive socket-patch in-process (commands::*::run) and via subprocess against wiremock-backed API fixtures, asserting on disk state, JSON envelope shape, and exit codes. Includes: * apply: invariants test (no .socket/ mutation), network tests with wiremock, edge cases (read-only files, nested dirs, multi-file, hash mismatch, idempotent re-apply, missing files, force overrides) * scan: invariants, sync end-to-end, dry-run preview, --apply + --prune combinations * get: identifier-type detection (UUID/CVE/GHSA/PURL/package), --save-only path, paid_required path, error paths, edge cases * repair: download-mode variants (file/diff/package), offline mode, blob cleanup verification * remove: PURL + UUID identifiers, rollback chain, blob cleanup * rollback: real bytes restore for all 8 ecosystems via handcrafted fixtures + real installer paths * setup: package.json detection, pnpm monorepo handling, dry-run * PTY-driven interactive prompt tests (portable-pty) * Alternate installer configs: yarn, pnpm, npm workspaces, bundler * Python venv variants: 3.11/3.12/3.13, .env/venv/.venv layouts, VIRTUAL_ENV override, canonical name normalization, egg-info legacy Real package managers are used where available on host (npm, pip, gem, cargo); ecosystems without host toolchains (go/maven/composer/ nuget) use handcrafted fixtures that exactly mirror what their native installers produce on disk. * ci: add coverage job, language version pins, and e2e-docker matrix * New 'coverage' job: cargo-llvm-cov (LLVM source-based instrumentation via taiki-e/install-action), uploads lcov.info as a workflow artifact and prints the summary to the GitHub Actions job summary. Report-only (no --fail-under threshold) so contributors get visibility without flaky CI when coverage shifts. * Language toolchain pins on every setup-* action invocation: Node 20.20.2, Python 3.12.13, Ruby 3.2.11. dtolnay/rust-toolchain now reads from rust-toolchain.toml (the toolchain: stable input is dropped from every step). * New 'e2e-docker' matrix: ubuntu-latest x { npm, pypi, gem, cargo, golang, maven, composer, nuget }. Each slot builds the shared base image and the per-ecosystem layer via docker/build-push-action with scope-cached layers (type=gha,scope=test-<eco>), then runs the corresponding 'cargo test --features docker-e2e --test docker_e2e_<eco>'. Triggered on every PR. The existing 'e2e' job (real Socket API, --ignored) stays for nightly/manual real-API smoke runs. * test(docker-e2e): infrastructure + npm full install→apply chain Adds the Docker-driven e2e test infrastructure: * tests/docker/Dockerfile.base: multi-stage build (rust:1.93-slim- bookworm builder → debian:12-slim runtime + compiled socket-patch). Base layer shared by every ecosystem image. Both base images pinned by sha256 digest. * tests/docker/Dockerfile.npm: FROM base + Node 20 LTS via NodeSource. * tests/docker/README.md: how to build images locally, run tests with Docker or with SOCKET_PATCH_TEST_HOST=1 host mode, and how to add a new ecosystem. * tests/docker/fixtures/npm/README.md: documents the synthetic fixture approach (--force apply against any installed bytes). * docker_e2e_npm.rs: real 'npm install minimist@1.2.2' inside the container, wiremock served patch, scan --sync writes manifest + blob, apply --force overwrites the on-disk file, then grep-verifies SOCKET-PATCH-E2E-MARKER in node_modules/minimist/index.js. Hermetic (no Socket API contact); reproducible in CI. This is the working template every other ecosystem extends. * test(e2e): full install→apply chain for pypi in Docker (+ global) Upgrades docker_e2e_pypi.rs from scan-discovery-only to the full chain, twice: once for local (venv) install and once for global (pip --break-system-packages). * Switches the fixture package from pydantic-ai (heavy transitive deps, ~60s install) to six 1.16.0 (single-file, ~1s install). * pypi's file-path convention has NO `package/` prefix — the python crawler returns site-packages root as pkg_path, so the patch's file path is just `six.py` (lands at site-packages/six.py). * `pypi_local_install_full_apply_chain`: venv install at .venv/lib/ python3.X/site-packages/six.py, scan --sync writes manifest + blob, apply --force --offline overwrites the file. Grep verifies SOCKET-PATCH-E2E-MARKER on disk. * `pypi_global_install_full_apply_chain`: pip install --break- system-packages installs into Debian's system Python site- packages. scan + apply with --global. Same marker verification at the system-site-packages path discovered via `python3 -c "import six; print(six.__file__)"`. The Dockerfile.pypi already has python3 + pip + venv from prior infrastructure work; no Dockerfile change. * test(e2e): full install→apply chain for gem in Docker (+ global) Two tests for the Ruby ecosystem: * gem_local_install_full_apply_chain: `gem install --install-dir vendor/bundle/ruby/<ver> colorize -v 1.1.0` produces the bundle- style layout that the Ruby crawler scans in local mode. scan --sync + apply --force overwrites lib/colorize.rb with the synthetic patched content; marker verified on disk. * gem_global_install_full_apply_chain: plain `gem install colorize -v 1.1.0` (no --install-dir) installs to `$(gem env gemdir)`. The --global flag drives the Ruby crawler to scan the system gem dir. Same marker check at the discovered path. gem patches use the `package/<rel>` convention; apply strips the `package/` prefix and joins with the gem's directory. Dockerfile.gem is unchanged from prior infrastructure work. * test(e2e): full install→apply chain for cargo in Docker `cargo fetch` against a minimal project with `cfg-if = "=1.0.0"` populates `\$CARGO_HOME/registry/src/<index>/cfg-if-1.0.0/`. scan --sync writes the manifest + blob, apply --force --offline overwrites the registry-source `src/lib.rs` with patched bytes containing SOCKET-PATCH-E2E-MARKER. grep verifies on disk. Pre-chmods the registry source file to writable — cargo's source files are read-only by default and apply's own fix-permissions code covers the same path, but the chmod up-front keeps the test robust against changes there. Single test (no global variant): cargo's registry is the only cache, so local-vs-global is a no-op. Dockerfile.cargo unchanged from prior infrastructure work; it has rustup-installed Rust 1.93.1 with CARGO_HOME set. * test(e2e): full install→apply chain for golang in Docker `go mod download github.com/gin-gonic/gin@v1.9.1` populates `\$GOMODCACHE/github.com/gin-gonic/gin@v1.9.1/`. scan --sync writes the manifest + blob, apply --force --offline overwrites gin.go with synthetic patched bytes containing SOCKET-PATCH-E2E-MARKER. grep verifies on disk. Pre-chmods the cache file to writable — `go mod download` extracts to read-only files, similar to cargo registry. Single test (no global variant): golang's module cache is the only cache; --global is a no-op. Dockerfile.golang ships Go 1.21.13 from the official tarball; GOPATH and GOMODCACHE are set at image build time. * test(e2e): full install→apply chain for maven in Docker Upgrades docker_e2e_maven.rs from scan-only to the full chain. `mvn dependency:get -Dartifact=org.apache.commons:commons-lang3:3.12.0` downloads the artifact into ~/.m2/repository, the wiremock fixture overwrites the .pom file with synthetic patched bytes, and the test grep-verifies SOCKET-PATCH-E2E-MARKER on disk. Single test (local-only) since ~/.m2 is always global. * test(e2e): full install→apply chain for composer in Docker Upgrades docker_e2e_composer.rs to the full chain plus a global variant. Real `composer require monolog/monolog:3.5.0` installs into vendor/monolog/monolog/, the wiremock fixture overwrites src/Monolog/Logger.php with synthetic patched bytes, and the test grep-verifies SOCKET-PATCH-E2E-MARKER on disk. Adds composer_global_install_full_apply_chain: `composer global require` installs to $COMPOSER_HOME/vendor, socket-patch scans + applies with --global, marker verified there. * test(e2e): full install→apply chain for nuget in Docker Upgrades docker_e2e_nuget.rs to the full chain plus a global variant. The local test redirects `dotnet add package` to a project-local ./packages dir via NUGET_PACKAGES, then scan + apply patch the package's LICENSE.md with a synthetic blob; the global test uses the default ~/.nuget/packages and --global mode. Note: the wiremock fixture uses the lowercased package name in the PURL ("newtonsoft.json") so scan's GC pass (--sync = --apply --prune) doesn't prune the freshly-saved manifest entry — the crawler reports installed packages by their lowercased directory name and GC keys against that. * test(e2e): add npm global install variant in Docker Adds npm_global_install_full_apply_chain alongside the existing local install/apply/rollback test. The variant runs `npm install -g`, locates the file at $(npm root -g)/minimist/index.js, then runs scan + apply with --global and grep-verifies SOCKET-PATCH-E2E-MARKER. Host-mode skips the global variant (no safe host npm prefix to mutate); Docker is the canonical run path. * test(e2e): add composer/maven/nuget Dockerfiles Adds the three Dockerfile recipes the docker_e2e_{composer,maven,nuget} tests panic-message instruct users to build. - Dockerfile.composer: base + PHP 8 + Composer 2 - Dockerfile.maven: base + default-jdk-headless + maven - Dockerfile.nuget: mcr.microsoft.com/dotnet/sdk:8.0 (sdk image) with socket-patch COPY'd in from the base * ci(coverage): include docker-e2e in the coverage map The host `coverage` job ran with `--all-features`, which enabled the docker-e2e feature, but the job never built the per-ecosystem Docker images — every docker_e2e_<eco> test would panic on `assert_image` and the job failed (or, if it ever passed, only the surviving in-process tests contributed). The Docker tests exercise the real socket-patch binary inside a Linux container, and that subprocess's coverage wasn't captured at all. Changes: * Each `docker_e2e_<eco>.rs` now reads SOCKET_PATCH_COV_BIN + SOCKET_PATCH_COV_PROFRAW_DIR. When both are set, the docker run mounts an llvm-cov-instrumented socket-patch binary over the image's baked-in /usr/local/bin/socket-patch and points LLVM_PROFILE_FILE into a host-visible volume. Empty Vec when unset → tests behave exactly as before for local dev and the existing e2e-docker matrix. * `coverage` job: drops `--all-features` for an explicit feature list (cargo,golang,maven,composer,nuget) that excludes docker-e2e. Produces `coverage-host.lcov`. * New `coverage-docker` matrix job: per ecosystem, builds the base + ecosystem Docker images, eval-sources `cargo llvm-cov show-env` to build an instrumented `target/debug/socket-patch`, sets the SOCKET_PATCH_COV_* hooks, runs `cargo llvm-cov --no-report --test docker_e2e_<eco>`, and emits a per-ecosystem lcov artifact. * New `coverage-merge` job: gathers `coverage-host` + all 8 `coverage-docker-*` artifacts and unions them via `lcov --add-tracefile` into a single `coverage-lcov` artifact. Same artifact name as before so downstream consumers keep working. Result: lines hit by ANY test (host in-process, host harness, or in-container binary execution) show up in the final coverage map. * ci: remove cargo cache from docker-image-building jobs zizmor's cache-poisoning audit (high) flagged the cargo `actions/cache` steps in `e2e-docker` and `coverage-docker` because both jobs also invoke `docker/build-push-action`. The risk model: a PR could poison the cargo cache (target/, ~/.cargo) with a backdoored crate or compiled object, and a later run on a trusted ref could load the poisoned cache and produce a compromised binary that gets mounted into the docker container or baked into the published image. Drop the cargo cache from both jobs. The Docker buildx `cache-from: type=gha` remains, so image-layer rebuilds are still fast. Cargo deps refresh from the registry per run — about a one-minute cost that's worth it to eliminate the attack surface. The other jobs (clippy, test, test-release, coverage, e2e) keep their cargo caches — none of them build Docker images, so the audit doesn't trigger for them. * ci: provide explicit toolchain input to dtolnay/rust-toolchain The action requires a `toolchain` input — when SHA-pinned (which is our policy), the action can't infer the channel from action_ref the way `@stable`/`@1.93.1` ref pins would, so it errors out with "'toolchain' is a required input". The original comments ("toolchain version is read from rust-toolchain.toml") referred to rustup's behavior after install, not the action's pre-install resolution — the action doesn't read rust-toolchain.toml itself. Set `toolchain: "1.93.1"` on every Install Rust step, matching the channel in rust-toolchain.toml. The duplication is intentional: if they drift, rustup will reconcile by installing the toolchain.toml channel on first cargo invocation, just at a small extra cost. * ci: drop dtolnay/rust-toolchain in favor of inline rustup Removes the third-party Rust toolchain action and replaces every "Install Rust" step with `rustup show`. rustup is pre-installed on GitHub-hosted runners; `rustup show` consumes rust-toolchain.toml, auto-installs the pinned channel if missing, and applies the listed components (rustfmt, clippy). For coverage jobs that additionally need llvm-tools-preview, `rustup component add llvm-tools-preview` follows the show step. Benefits: - One less third-party action to audit and SHA-pin. - No duplication between rust-toolchain.toml and ci.yml. - Toolchain bumps are one-file changes (just edit toolchain.toml). * refactor(cli): unify CLI args + env-var bindings across every subcommand Define a single `GlobalArgs` clap struct and `#[command(flatten)]` it into every subcommand's args. Every flag now has a matching `SOCKET_*` env var binding (precedence: CLI > env > default). Legacy `SOCKET_PATCH_PROXY_URL`, `SOCKET_PATCH_DEBUG`, `SOCKET_PATCH_TELEMETRY_DISABLED` are still honored at runtime via a one-shot deprecation warning that fires even under `--silent` / `--json`. Behavior changes: - `--offline` now means strict airgap on every command (was three different things across apply / repair / rollback). On `repair`, `--offline` and `--download-only` are mutually exclusive. - `repair --download-mode` default flipped from `file` to `diff` to match every other command. Users who need the legacy per-file blob behavior opt in with `--download-mode file`. - `apply` and `repair` gain `--api-url` / `--api-token` / `--org` for free via the flatten (previously only readable via env). - `--debug` and `--no-telemetry` promoted from env-only toggles to CLI flags. CLI_CONTRACT.md rewritten around a single global-args table plus a small per-subcommand section for local flags. New tests: `cli_global_args.rs` (compose test: every global flag × every subcommand) and `cli_env_deprecation.rs` (legacy-env warning fires under `--silent` / `--json`). Assisted-by: Claude Code:opus-4-7 * docs(changelog): add CHANGELOG.md + CI guard blocking publish without entry Adds a Keep-a-Changelog-style CHANGELOG.md at the repo root, backfilled with concise summaries for every published tag (v1.1.0 → v2.1.4) and a detailed v3.0.0 entry covering the breaking changes in the in-flight v3 release (unified `--offline`, `repair --download-mode` default flip, `SOCKET_PATCH_*` → `SOCKET_*` env-var renames with one-shot deprecation warning, shared `GlobalArgs` flatten across every subcommand, etc.). Wires a new step into the `Release` workflow's `version` job that fails the workflow when `CHANGELOG.md` lacks an entry for the version in Cargo.toml. Because every downstream job (tag, build, github-release, cargo/npm/pypi-publish) transitively depends on `version`, a missing changelog entry blocks the entire publish pipeline. Accepts both `## [X.Y.Z]` and `## X.Y.Z` heading styles to keep the format requirement loose for future contributors. Assisted-by: Claude Code:opus-4-7 * ci(test): unbreak test/coverage/e2e-docker matrix on feat/scan-apply-json This commit fixes the pre-existing CI red on the v3.0 branch. Four unrelated root causes: 1. `cargo test --workspace --all-features` enables the `docker-e2e` feature, which compiles the 8 `docker_e2e_<eco>.rs` tests on every `test (ubuntu/macos/windows)` runner. Those tests `assert_image()` on a docker image that only exists in the dedicated docker-building jobs, so every test runner failed. Replaced each `assert_image()` panic with a `skip_if_no_image()` early return that prints a stderr skip notice. Tests now report `ok` on hosts without docker / images. `cargo test --workspace --all-features` is green everywhere. 2. The `coverage` job (cargo-llvm-cov, --all-features) failed three `in_process_remove_repair_lifecycle` tests that set `SOCKET_API_URL`/`SOCKET_API_TOKEN`/`SOCKET_ORG_SLUG` via `std::env::set_var` after constructing `RepairArgs` via `..GlobalArgs::default()`. The refactor's `api_client_overrides()` was always forwarding the resolved api_url/proxy_url as `Some(...)`, which short-circuited the env-var fallback inside `get_api_client_with_overrides`. Made `GlobalArgs::default()` leave `api_url`/`proxy_url` empty (clap always populates them in production via `default_value`, so the production path is unchanged) and `api_client_overrides()` filters empty values to `None`. The env-var fallback now fires for these tests. 3. `repair_download_only_skips_cleanup` (in `repair_invariants.rs`) used the shared `run_repair()` helper which injects `--offline`. v3.0 made `--offline` and `--download-only` mutually exclusive (exit code 2). Inlined the binary invocation without `--offline` for this one test — the manifest's referenced blob is already on disk so the download phase is a no-op even without `--offline`. 4. The `e2e-docker` and `coverage-docker` matrix jobs failed at "Build <eco> image" with `pull access denied` on `socket-patch-test-base:latest`. setup-buildx-action defaults to the `docker-container` driver, which runs BuildKit in a sandboxed container that cannot see the host docker daemon's image store — so the per-ecosystem Dockerfile's `FROM socket-patch-test-base:latest` tries to pull from docker.io and fails. Switched both jobs to `driver: docker` so buildx talks to the host daemon directly. Dropped the `type=gha` cache directives (not supported under the docker driver) — we trade build cache for image visibility. Local: `cargo test --workspace --all-features` → 965 passed, 0 failed. Assisted-by: Claude Code:opus-4-7 * ci(coverage-docker): pin to ubuntu-22.04 for glibc compatibility The coverage-docker matrix builds an instrumented socket-patch binary on the host and mounts it into the debian:12-slim test container. ubuntu-latest is currently 24.04 (glibc 2.39); debian:12 ships glibc 2.36. Result: every coverage-docker matrix job failed with socket-patch: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by socket-patch) Pin to ubuntu-22.04 (glibc 2.35) — the highest base that's forward-compatible with debian:12. e2e-docker is unaffected because it runs the binary that was baked into the image by the base Dockerfile's internal builder stage, not a host-mounted one. Assisted-by: Claude Code:opus-4-7 * test(npm): connect via 127.0.0.1 in infrastructure smoke (Windows fix) wiremock binds to 0.0.0.0 (the wildcard). Linux and macOS quietly route a connect to 0.0.0.0 onto the loopback interface, so the test worked on those runners. Windows refuses the connect with WSAEADDRNOTAVAIL (winsock error 10049) because 0.0.0.0 is a valid bind target but not a valid destination address. Use 127.0.0.1 explicitly for the smoke-check URL — the bound port from `server.address().port()` is still what we need. Assisted-by: Claude Code:opus-4-7 * test(pypi): skip in_process_pypi_apply on Windows (Unix venv layout) `install_six()` hardcodes `venv/bin/pip` and `find_site_packages()` walks `venv/lib/pythonX.Y/site-packages/`. Both layouts are Unix-only — on Windows the venv puts pip at `Scripts\pip.exe` and site-packages at `Lib\site-packages\` (no per-version subdirectory). Rather than forking the helpers per platform, gate every test in this file behind `skip_unsupported_platform()` which prints a skip notice on Windows and returns early. The same code paths get exercised by the Linux test runner and the docker_e2e_pypi suite, so coverage isn't lost. Assisted-by: Claude Code:opus-4-7 * test(pypi): make in_process_pypi_apply helpers platform-aware (Windows) Reverts the earlier Windows-skip in favor of real Windows coverage. Three changes to the helpers: 1. find_python() probes `python3` → `python` → `py` (mirrors the crawler's `find_python_command` in core/src/crawlers/python_crawler.rs:15). On Windows the canonical name is `python` (the `py` launcher is also installed); `python3` is rare. Without this the venv-creation step calls `python3` and fails on every Windows runner. 2. venv_pip() returns `Scripts\pip.exe` on Windows vs `bin/pip` on Unix, matching PEP-405's documented venv layout. 3. find_site_packages() branches on cfg!(windows): * Windows: `<venv>\Lib\site-packages\` — no version subdirectory. * Unix: glob `<venv>/lib/python3.X/site-packages/` for whatever interpreter version pip used. The four in-process tests now exercise the same install→scan→apply chain on Windows that they already cover on Linux/macOS. The core crawler is already Windows-aware (python_crawler.rs:182) so the package-discovery path it tests is real, not synthetic. Assisted-by: Claude Code:opus-4-7 * test(rollback): use Windows venv layout when cfg!(windows) rollback_pypi_restores_original_content set up a synthetic `.venv/lib/python3.11/site-packages/` tree by hand. That's the Unix layout — on Windows the pypi crawler at core/src/crawlers/python_crawler.rs:182 looks for `.venv\Lib\site-packages\`, so on Windows runners the crawler found nothing and the patched file was never rolled back. Branch on `cfg!(windows)` when building the path so the synthetic package sits where the crawler actually probes on each platform. The crawler logic itself is unchanged. Assisted-by: Claude Code:opus-4-7 * feat(json): enrich added/updated patch records with description, severity, vuln IDs When `get` or `scan --apply` adds or updates a patch in the manifest, the per-patch JSON record now carries the metadata consumers need to render the patch to a human without a second API round-trip: ```jsonc { "purl": "pkg:npm/minimist@1.2.2", "uuid": "11111111-...", "action": "added", "description": "Fixes prototype pollution in minimist", "license": "MIT", "tier": "free", "exportedAt": "2024-01-01T00:00:00Z", "severity": "high", "vulnerabilities": [ { "id": "GHSA-xvch-5gv4-984h", "cves": ["CVE-2024-12345"], "severity": "high", "summary": "Prototype Pollution", "description": "merge() does not check Object.prototype" } ] } ``` Highlights: - Top-level `severity` is the max across the vulnerabilities array, using the ordering critical > high > medium=moderate > low. - `vulnerabilities[]` is sorted by `id` so consumer diffs and test snapshots don't flap on HashMap iteration order. - Metadata is intentionally omitted on `action: skipped` (consumer already has it from the original add) and on `action: failed`. - `scan --apply` benefits automatically — both flows go through `download_and_apply_patches`. Helpers `severity_rank`, `max_vuln_severity`, `patch_event_metadata` are pub(crate) and unit-tested. CLI_CONTRACT.md gains a new "`patches[]` entry shape" subsection documenting the schema. Assisted-by: Claude Code:opus-4-7 * test(e2e): walk the v3.0 envelope for `list --json` output The e2e (real-registry) suite was asserting on `list["patches"]` — the pre-v3 ad-hoc shape. v3.0 migrated `list --json` to the unified envelope, which emits `{command, status, events, summary}` with one `discovered` event per manifest entry. Patch metadata (vulnerabilities, tier, license) lives under `details`. Updated four sites (e2e_npm × 2, e2e_pypi × 1, e2e_gem × 1) to filter events by `action == "discovered"` and walk `details.vulnerabilities[]` for CVE assertions. Closes the `e2e (ubuntu/macos, e2e_npm|e2e_pypi|e2e_gem)` matrix failures surfaced once the e2e workflow started passing on the v3.0 branch. Assisted-by: Claude Code:opus-4-7 * ci(e2e_gem): bump pinned ruby from 3.2.11 to 3.2.10 `ruby/setup-ruby` dropped 3.2.11 from its catalog at some point — the action errors with "Unknown version 3.2.11 for ruby on ubuntu-24.04" and lists 3.2.10 as the newest 3.2.x available. 3.2.x is API-stable so 3.2.10 is a drop-in replacement. Assisted-by: Claude Code:opus-4-7 * refactor(cli): drop -d/-m short aliases; loosen version pins Two unrelated changes in one commit: 1. Drop the `-d` short for `--dry-run` and `-m` short for `--manifest-path` from `GlobalArgs`. We want those letters free for future flags. The long forms are unaffected, and a new `reserved_short_forms_are_not_assigned` compose test locks in that no subcommand reassigns either letter. Per-subcommand short-form tests (`*_short`, `manifest_path_short_form`, etc.) are deleted; the long-form counterparts cover the contract. 2. Loosen `python-version` and `ruby-version` pins in ci.yml from exact patch (`3.12.13`, `3.2.10`) to minor.x (`3.12.x`, `3.2.x`). setup-python and setup-ruby's catalogs keep retiring older patch versions and breaking the workflow — minor.x auto-resolves to whatever patch is currently available. CLI_CONTRACT.md updated to remove `-d`/`-m` from the global args table and the env-var cross-reference. Assisted-by: Claude Code:opus-4-7 * feat(apply): preserve mode + ownership across patches `apply_file_patch` now treats target-file permissions as a strict round-trip: 1. **Existing file**: snapshot mode + uid + gid before writing. - If read-only, temporarily grant owner-write so the overwrite succeeds (Go module cache, npm linked symlinks, etc.). - After writing, restore the *exact* pre-patch mode (idempotent `set_permissions(from_mode(...))`) and chown back to the pre-patch uid/gid. `tokio::fs::write` truncates + rewrites the file in place, so owner usually survives, but pinning ownership explicitly stops a theoretical race where another process opens the file between truncate and write. 2. **New file** (created by the patch): chown to inherit owner/group from the parent directory, mode = `0o444` (read-only for all). Matches how a freshly-unpacked package tarball treats its files. Windows: no uid/gid concept; preserve the readonly attribute for existing files and force it on new ones. `restore_file_permissions` and the `chown_blocking` helper are split out of `apply_file_patch` for readability and unit testing. Four new tests pin the policy: readonly-mode preservation, executable (0o755) mode preservation, new-file default mode + parent ownership inheritance, and uid/gid round-trip on existing files. Assisted-by: Claude Code:opus-4-7 * ci(e2e_gem): revert ruby-version to exact 3.2.10 setup-ruby (unlike setup-python) does NOT support the `3.2.x` wildcard pin — it errors with "Unknown version 3.2.x for ruby on ubuntu-24.04". Revert to an exact patch that's in the catalog. When this patch eventually drops off, bump it manually per the list at https://github.com/ruby/setup-ruby. Assisted-by: Claude Code:opus-4-7
Configuration menu - View commit details
-
Copy full SHA for 9b7b5c2 - Browse repository at this point
Copy the full SHA 9b7b5c2View commit details
Commits on May 23, 2026
-
feat(apply): safety hardening — atomicity, locking, pnpm CoW, sidecar…
…s, Maven gate (#80) * feat(apply): safety primitives — lock, CoW, atomic write, sidecar fixups Adds five new modules to `socket-patch-core` and refactors `apply_file_patch` to compose them safely with #79's perm-preservation: - **`patch::apply_lock`** — cross-platform advisory file lock at `<.socket>/apply.lock` via `fs2`. Used by every mutating subcommand to serialize against concurrent socket-patch runs. - **`patch::cow`** — hardlink + symlink copy-on-write. Before patching, if `filepath` is a symlink into a content-addressed store (pnpm) or a regular file with `nlink > 1` (bazel mirrors, nix store overlays), give this project a private inode. The pnpm content store and every other project pointing at it stay byte-identical. - **`patch::sidecars`** — ecosystem-aware sidecar fixups dispatched from `apply_package_patch`. Cargo: rewrite `.cargo-checksum.json` with new SHA256s so `cargo build` accepts patched sources. NuGet: delete `.nupkg.metadata` (the documented "unknown" state vs. a stale `contentHash` that would flag tampering). PyPI / gem / Go: advisory-only — surface a one-line note about downstream tooling consequences. - **`crawlers::pkg_managers`** — path-based detector for the four Node.js layout flavors (npm / pnpm / yarn-classic / yarn-berry PnP). Apply uses this to refuse yarn-berry PnP (packages live in `.yarn/cache/*.zip`) and to surface a pnpm-detected note. - **`apply_file_patch` atomic rewrite** — two-phase commit: 1. Hash `patched_content` in memory; error out before any disk write if it doesn't match `expected_hash`. Removes the prior "wrote bytes, post-write verify failed, can't restore" window. 2. CoW the target if it's a shared inode. 3. Stage write to `<parent>/.socket-stage-<uuid>`, `sync_all()`, then `rename(stage, target)`. POSIX `rename(2)` is atomic — observers see either the old or new bytes, never a truncated half-write. Composes cleanly with #79's mode + uid/gid restore step which now operates on the post-rename inode. `ApplyResult` grows `sidecars_updated: Vec<String>` and `sidecar_advisory: Option<String>` so the CLI envelope can surface fixup outcomes. `fs2` and `tempfile` added to socket-patch-core dependencies. Two new tests pin the headline invariants: - `test_apply_file_patch_hash_mismatch_leaves_original_intact` — atomic-write contract: hash mismatch leaves target byte-identical AND no `.socket-stage-*` litter in parent dir. - `test_apply_file_patch_does_not_propagate_to_hardlinked_sibling` — the pnpm content-store invariant at the integration level. Plus 10 unit tests for cow + apply_lock and 13 for sidecars/* + 9 for pkg_managers. Assisted-by: Claude Code:claude-opus-4-7 * feat(cli): wire safety primitives + Maven/NuGet experimental gates Integrates the new socket-patch-core safety primitives into the CLI via the v3.0 unified `GlobalArgs` + `Envelope` patterns from #79. **`commands::lock_cli`** (new) — envelope-aware wrapper around `apply_lock::acquire`. Takes `Command` so the failure envelope's `command` field reflects which subcommand was blocked. On contention the binary emits `{status: "error", error: {code: "lock_held", ...}}` in JSON mode or a one-line stderr message otherwise, then exits 1. **Lock acquisition** added to `apply`, `rollback`, `repair`, `remove` immediately after the manifest existence check. `remove`'s outer lock spans the inner `rollback_patches` call (which deliberately does NOT acquire the lock so the composition doesn't self-deadlock). **Apply pkg-manager gating** — after the lock, `apply` runs `detect_npm_pkg_manager`: - `YarnBerryPnP` → emit `EnvelopeError("yarn_pnp_unsupported", ...)` pointing at `yarn patch` and exit 1. - `Pnpm` → surface a one-line stderr note. CoW handles the substantive safety work; this just tells the user the layout was understood. **Sidecar JSON via `event.details`** — `result_to_event` extends the Applied event with `details.sidecarsUpdated: string[]` and `details.sidecarAdvisory: string | null` when either is non-empty. Narrower JSON-envelope contract than first-class fields; consumers read `event.details.sidecarsUpdated` from JSON. **Maven + NuGet experimental runtime gates** in `ecosystem_dispatch.rs`. Even when compiled with `--features maven`/`nuget`, the crawlers refuse to dispatch unless the matching `SOCKET_EXPERIMENTAL_MAVEN=1`/`SOCKET_EXPERIMENTAL_NUGET=1` env var is set. Without it, surface a warning event and skip those PURLs. Reasoning: Maven patches corrupt jar sidecar checksums (sha1/md5); NuGet patches corrupt `.nupkg.sha512` signature sidecars that `dotnet restore` reads as tamper-evidence. `fs2` added to socket-patch-cli dev-dependencies for the lock e2e test (same crate the binary uses internally). Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): safety hardening suite + CI matrix + invariant fixups Adds four end-to-end integration test files exercising the safety primitives through the binary, plus shared `tests/common/mod.rs` helpers, plus two existing-test contract updates. **Suites added (20 new tests):** - `e2e_safety_lock.rs` (6 tests, non-ignored). Test holds the same `.socket/apply.lock` the binary uses via `fs2` directly, then spawns `socket-patch apply` and asserts the second process exits with `error.code == "lock_held"`. Zero production-code hooks. - `e2e_safety_yarn_pnp.rs` (5 tests, non-ignored). Yarn-berry PnP markers (`.pnp.cjs`, `.pnp.loader.mjs`) trigger `error.code == "yarn_pnp_unsupported"`. Negative control: plain npm layout does NOT trigger the refusal. - `e2e_safety_cargo_build.rs` (5 tests, `#[ignore]` + `--features cargo`). Three synthetic-vendor tests: 1. Baseline `cargo check --offline --frozen` succeeds. 2. Negative control — mutating the source WITHOUT the sidecar fixup makes cargo refuse with "checksum changed". Proves cargo actually verifies, which is what makes the positive test meaningful. 3. Sidecar fixup makes `cargo check` pass; `.cargo-checksum.json` is rewritten and the `package` field is preserved. 4. JSON envelope contract: `.cargo-checksum.json` appears in `event.details.sidecarsUpdated`. Plus `traitobject_real_socket_patch_round_trip` — the cargo layer-2+3 combined test: `cargo fetch traitobject@0.0.1` from crates.io → `socket-patch get b15f2b7f-d5cb-43c9-b793-80f71682188f` from patches-api.socket.dev → assert `.cargo-checksum.json` rewritten + `cargo check` succeeds against the real, production Socket patch. - `e2e_safety_pnpm.rs` (4 tests, `#[ignore]`). Two projects share a pnpm content store via `--config.package-import-method=hardlink`. `socket-patch get` in project A patches A; project B + store entry stay byte-identical. `pnpm install --frozen-lockfile` in B afterwards does not revert A. Exercises CoW against a real pnpm install rather than a hand-rolled hardlink. **`tests/common/mod.rs`** — shared helpers (`binary`, `run`, `assert_run_ok`, `git_sha256`, `sha256_hex`, `pnpm_run`, `cargo_run`, `write_minimal_manifest`, `write_blob`, `parse_json_envelope`, `envelope_error_code`, `envelope_error_message`) lifted from the duplicated copies in `e2e_npm.rs` etc. Additive; existing suites keep their inlined copies for now. **CI matrix** in `.github/workflows/ci.yml`: - `e2e_safety_cargo_build` on ubuntu + macos + windows - `e2e_safety_pnpm` on ubuntu + macos + windows (pnpm-on-Windows uses junctions + copies by default, so the CoW invariant holds vacuously; the test still runs to verify apply doesn't error on Windows. Semantic Windows nlink coverage is a follow-up — `std::fs::Metadata` doesn't expose nlink on Windows without `GetFileInformationByHandle` via `windows-sys`.) - New `Setup pnpm` step (`npm install -g pnpm@10`) gated on the pnpm suite. The fast non-ignored suites (`e2e_safety_lock`, `e2e_safety_yarn_pnp`) run via the standard `test` job on all three platforms. **Existing-test contract updates** (these tests were pinning the old, broken behavior; both still describe correct invariants — their assertions just needed to track the rebased semantics): - `tests/apply_invariants.rs`: `dir_hash` excludes `apply.lock`. The lock file is deliberate ephemeral session state, not patch content; the "apply is read-only against .socket/" invariant is about manifest + blobs + diffs + packages. - `tests/in_process_edge_cases.rs`: `apply_blob_after_hash_mismatch_reports_failure` now asserts the atomic-write contract — the target file is byte-identical to its pre-call state on the hash-mismatch failure path, no half-written corruption. Assisted-by: Claude Code:claude-opus-4-7 * refactor(sidecars): typed envelope contract with structured per-file + advisory data Replaces the previous `event.details.sidecarsUpdated` / `event.details.sidecarAdvisory` free-form JSON bag with a typed, top-level `Envelope.sidecars[]` list. ## New types (`socket-patch-core/src/patch/sidecars/types.rs`) pub struct SidecarRecord { purl, ecosystem, files, advisory } pub struct SidecarFile { path, action: SidecarFileAction } pub enum SidecarFileAction { Rewritten | Deleted | Created } pub struct SidecarAdvisory { code, severity, message } pub enum SidecarAdvisoryCode { PypiRecordStale | GemBundleInstallReverts | GoModVerifyFails | NugetSignedPackageTampered | SidecarFixupFailed } pub enum SidecarSeverity { Info | Warning | Error } All derive `serde::Serialize`. Structs use camelCase; enums use snake_case. Unit tests pin the JSON contract. ## JSON shape (consumer view) ```json { "command": "apply", "events": [...], "sidecars": [ { "purl": "pkg:cargo/...", "ecosystem": "cargo", "files": [{"path":".cargo-checksum.json","action":"rewritten"}] }, { "purl": "pkg:nuget/...", "ecosystem": "nuget", "files": [{"path":".nupkg.metadata","action":"deleted"}], "advisory": { "code":"nuget_signed_package_tampered", "severity":"warning", "message":"..." } } ] } ``` - `sidecars` omitted from JSON when empty. - `files` always present (possibly `[]` for advisory-only). - `advisory` omitted when absent. - `code` / `severity` are stable snake_case enum tags; `message` is human text. - `purl` joins to `events[].purl` for per-event context. ## Three real improvements over the old design 1. **No more lossy collapse.** NuGet's "deleted `.nupkg.metadata` AND has a `.nupkg.sha512` signature" case now carries BOTH a file entry AND an advisory. Before, the advisory was silently lost when the file entry took its slot. 2. **Stable codes + severity.** Consumers (CI bots, dashboards, telemetry, jq pipelines) can switch on `code` and route on `severity` without regex-matching free-form strings. 3. **Decoupled from events.** Sidecar reporting is a top-level `Envelope.sidecars` list. `PatchEvent.details` is no longer mixed with `list` / `repair` / `remove`'s command-specific bags — sidecar consumers have a typed schema all their own. ## Internal refactor - `SidecarOutcome` removed. Per-ecosystem fixups return `Result<Option<SidecarPayload>, SidecarError>` (internal `SidecarPayload = { files, advisory }`); the dispatcher in `sidecars/mod.rs` wraps the payload with PURL + ecosystem to produce the `SidecarRecord`. - `ApplyResult.sidecars_updated: Vec<String>` and `sidecar_advisory: Option<String>` consolidated into a single `sidecar: Option<SidecarRecord>` field. - Apply CLI's `result_to_event` no longer attaches to `event.details`; the run loop now calls `env.record_sidecar(record.clone())` after each apply result. - `Envelope` gains `sidecars: Vec<SidecarRecord>` field + `record_sidecar` method. - The error path (`SidecarError` returned by a fixup) is converted at the apply boundary into a `SidecarRecord` with `advisory.code = SidecarFixupFailed`, `severity = Error`. Single uniform shape for consumers. ## Pre-existing test fixups `in_process_remote_ecosystems_apply.rs` and `in_process_rollback_all_ecosystems.rs` now set `SOCKET_EXPERIMENTAL_MAVEN=1` / `SOCKET_EXPERIMENTAL_NUGET=1` when they explicitly exercise those paths. These were broken silently by the Maven/NuGet runtime gates added in the prior rebase (the gate was always there in commit 39a2321; tests just happened not to exercise the maven/nuget paths to a depth where the skip mattered). ## Test results - cargo build --workspace --all-features: clean - cargo build --release --workspace: clean (no warnings) - cargo clippy --workspace --all-features -- -D warnings: clean - cargo test --workspace --all-features: 1021 passed, 0 failed - cargo test --features cargo --test e2e_safety_cargo_build -- --ignored: 5 passed (includes traitobject real-patch round trip) The e2e cargo test `apply_reports_cargo_checksum_in_sidecars_updated` tightened from a substring match to a structured-shape assertion on `envelope.sidecars[].ecosystem=="cargo"` + `files[].path=".cargo-checksum.json"` + `files[].action=="rewritten"`. Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): expand sidecar coverage + simplify PTY harness Five test surfaces, one bug fix, one YAGNI cleanup, one harness simplification — all motivated by closing the e2e gap on the new typed `Envelope.sidecars[]` contract. - **e2e_safety_advisories.rs** (new, 5 tests): drive the apply CLI against handcrafted layouts and assert `envelope.sidecars[].{ecosystem,advisory.code,advisory.severity, files[]}` for pypi (`pypi_record_stale`), gem (`gem_bundle_install_reverts`), golang (`go_mod_verify_fails`), nuget unsigned (deleted files only), and nuget signed (deleted files + `nuget_signed_package_tampered` advisory together — the case the pre-typed-contract design lost). - **e2e_safety_cow.rs** (new, 5 tests): cover `patch/cow.rs` end to end — hardlink isolation, symlink replacement, multi-file hardlink, regular-file no-op, and the failure-doesn't-cow path. Lifted file coverage from ~23% to ~80% (remaining gaps are defensive I/O error arms not reproducible in tests). - **e2e_safety_cargo_build.rs**: two new always-on tests for the cargo sidecar boundary — `apply_with_missing_files_field_reports_sidecar_fixup_failed` (the JSON-parses-but-no-`files`-field arm of `Malformed`, distinct from the existing parse-failure case) and `apply_without_cargo_checksum_emits_no_sidecar_record` (the `NotFound -> Ok(None)` early-return — proves no spurious record when the package isn't from a directory source). - **interactive_prompts_e2e.rs**: simplify the PTY harness. Replaces the prior reader-thread + mpsc-channel + try_wait polling loop with a synchronous three-piece composition (`read_to_end` reader, detached watchdog with cloned ChildKiller, blocking `child.wait()` on the main thread). No pre-write sleep — the PTY buffers input. All six prompt tests still pass with materially less harness code. - **common/mod.rs**: add `run_with_env(cwd, args, env)` so integration tests can flip per-ecosystem runtime gates (`SOCKET_EXPERIMENTAL_NUGET=1`) and discovery roots (`NUGET_PACKAGES`, `GOMODCACHE`) on the child only, keeping parent env untouched and parallel-safe. - **Bug fix**: `in_process_remote_ecosystems_apply.rs` and `in_process_rollback_all_ecosystems.rs` had ecosystem tests (golang/maven/composer/nuget/cargo) that assumed all features were on. Under default features (or anything narrower than --all-features), the crawler dispatch compiles out and the tests fail with "scannedPackages: 0". Gated each test on `#[cfg(feature = "<eco>")]` to match the build matrix. Quiet the resulting dead-code noise with a file-level allow. - **YAGNI**: drop `SidecarFileAction::Created`. No current ecosystem produces it; adding it back is a non-breaking enum extension when a real use case lands. All ~456 workspace tests pass under `--all-features`. Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): close remaining cargo + nuget sidecar fixup-error arms Three additional defensive-path tests, lifting sidecar coverage toward its e2e ceiling: - **cargo.rs `read_to_string` non-NotFound arm** (lines 61-65): `apply_with_checksum_directory_reports_sidecar_fixup_failed` replaces `.cargo-checksum.json` with a directory of the same name. `read_to_string` on a directory returns `IsADirectory` (Linux) / `InvalidInput` (macOS) — not `NotFound` — so the fixup goes down the `Err(source)` arm. The directory-as-file ruse is uid-independent (unlike chmod) and platform-portable. - **cargo.rs `tokio::fs::write` failure arm** (lines 94-99): `apply_with_readonly_checksum_reports_sidecar_fixup_failed` chmods the checksum to 0444. Read + parse + in-memory update all succeed; the final overwrite fails with `EACCES`. Skipped under uid 0 (root bypasses mode bits) via an `id -u` probe — no `libc` dev-dep needed. - **nuget.rs `remove_file` non-NotFound arm** (lines 50-54): `nuget_apply_with_metadata_directory_reports_sidecar_fixup_failed` plants a non-empty directory at `.nupkg.metadata`. `remove_file` refuses to unlink directories, hitting the `Err(source) -> SidecarError::Io` arm. Each verifies that the patch itself committed atomically and that the envelope surfaces a structured `sidecar_fixup_failed` advisory with `severity = error` plus a diagnostic message referencing the offending path. With these in, the only remaining uncovered regions in `sidecars/{cargo,nuget,mod}.rs` are: - `cargo.rs:89-91` — `serde_json::to_vec_pretty` on a Value just parsed from valid JSON. Unreachable without UB. - `cargo.rs:126-128` — `sha256_file` of a file `apply` just atomically wrote. Race-only. - `sidecars/mod.rs:110, 115` — `patched.is_empty()` and unknown PURL guards, both gated by upstream apply.rs checks. - `nuget.rs:86, 93` — `read_dir` on a found package dir, and a non-UTF8 file name. No realistic e2e path. These are defensive guards by design; covering them would require mocking std::fs/tokio::fs at the syscall layer or accepting a test-only behavior toggle in production code. The lib unit tests already exercise the guards that matter. Coverage delta (regions, integration-test-only): sidecars/cargo.rs 76.7% → 90.1% sidecars/nuget.rs 91.4% → 96.6% sidecars/mod.rs 93.6% → 95.7% Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): close internals guards + nuget non-UTF8 iteration arm Adds an `e2e_safety_internals.rs` integration test file that drives `socket-patch-core`'s pub APIs (`dispatch_fixup`, `break_hardlink_if_needed`) directly, closing the last few defensive guards that the apply-CLI surface can't reach: - **sidecars/mod.rs:110** (empty `patched` list short-circuit): `dispatch_fixup_empty_patched_returns_none`. - **sidecars/mod.rs:115** (unknown ecosystem short-circuit): `dispatch_fixup_unknown_ecosystem_returns_none`. - **cow.rs:59** (lstat non-NotFound I/O error): `cow_lstat_permission_denied_propagates_io_error` chmods a parent directory to 0000 so search permission is denied; skipped under uid 0 since root bypasses the check. - **cow.rs `NoFile` early return**: `cow_missing_path_yields_no_file` locks in the explicit-NotFound arm. Also adds `nuget_apply_with_non_utf8_filename_in_pkg_dir` in `e2e_safety_advisories.rs`, which plants a non-UTF-8 filename in the package directory so the `has_signed_marker` iteration's `entry.file_name().to_str() => None` arm fires (nuget.rs:93). Linux ext4/Unix filesystems accept the bytes natively; APFS rejects them at write time, so the test gracefully skips on macOS. `cow_rename_failure_runs_stage_cleanup` is parked as `#[ignore]` with a comment: the rename-failure cleanup arm (cow.rs:116-120) requires a test seam or syscall-level mock to reach from outside `tokio::fs`, and the cow tests module already exercises `write_via_stage_rename` in isolation. Final integration coverage of the touched files (regions): sidecars/mod.rs 96.4% → 100.0% sidecars/cargo.rs 76.7% → 90.1% sidecars/nuget.rs 91.4% → 96.6% (locally; Linux CI bumps to ~98%) patch/cow.rs 79.0% → 86.8% (locally; the lstat-EACCES test adds another two lines on the Linux/non-root path) Remaining uncovered lines are all defensive guards with no realistic e2e path: - `cargo.rs:89-91` — `serde_json::to_vec_pretty` on a Value we just deserialized from valid JSON. Total function; cannot fail. - `cargo.rs:126-128` — `sha256_file` of a file `apply` just atomically wrote. Race-only. - `nuget.rs:86` — `read_dir` error on a directory we just read packages from. Race-only. - `cow.rs:116-120` — `rename` failure inside `write_via_stage_rename`. Race-only without a test seam. Workspace test sweep: 456 passed / 0 failed under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): exercise sidecar/cow defensive arms via direct dispatch Layers three engine-direct integration tests on top of the apply-CLI suite to close the remaining defensive paths that the CLI flow can't naturally reach, plus a small production cleanup of one genuinely- dead error arm in cargo.rs. ## Production change **`sidecars/cargo.rs`**: replace the `serde_json::to_vec_pretty(&v).map_err(...)?` construction with `.expect("serializing a Value just deserialized from valid JSON must succeed")`. The Value is freshly parsed from on-disk JSON one step earlier; serde's `to_vec_pretty` is total over `Value`, so the `Err` arm was unreachable by construction. The `.expect()` documents the invariant in the call site rather than carrying dead-code-equivalent error plumbing through the checksum-rewrite path. ## New direct-dispatch tests (e2e_safety_internals.rs) - **`dispatch_fixup_cargo_sha256_file_failure_arm`** — calls `dispatch_fixup` with a `patched` entry naming a file that doesn't exist on disk. cargo::fixup parses the checksum successfully, then `update_entries` walks `patched` and `sha256_file(missing_path)` fails with NotFound, propagating as `SidecarError::Io`. Covers `cargo.rs:131-133`. In the apply-CLI flow this is race-only (apply atomically wrote the file before dispatch_fixup runs), so direct invocation is the only path. - **`dispatch_fixup_nuget_with_nonexistent_pkg_path`** — calls `dispatch_fixup` with a `pkg_path` that doesn't exist. Inside nuget::fixup, `remove_file(.nupkg.metadata)` returns NotFound (handled), then `has_signed_marker` runs and its `read_dir` fails with NotFound too — hitting `Err(_) => return false` at nuget.rs:86. Fixup returns `Ok(None)`. Same race-only-from-CLI caveat. - **`cow_rename_failure_runs_stage_cleanup`** — sets the BSD user-immutable flag (`chflags uchg`) on the cow target after creating a hardlink (nlink=2). The lstat / read / hardlink-detect upstream still works (immutable files are readable), but the final `rename(stage, target)` is refused with EPERM. The test asserts the error propagates AND that the cleanup arm (cow.rs:117-119) ran — no `.socket-cow-*` stage file is left in the directory. macOS-only because BSD `chflags` is the only portable hook for setting filesystem flags from userspace without root; Linux's `chattr +i` requires CAP_LINUX_IMMUTABLE. Both macOS and Linux skip uid 0 (root bypasses uchg/immutable). ## Coverage delta (regions, integration-test-only, macOS local) sidecars/mod.rs 100.0% → 100.0% (unchanged; already at ceiling) sidecars/cargo.rs 94.9% → 100.0% sidecars/nuget.rs 95.2% → 97.6% patch/cow.rs 86.8% → 94.7% The only macOS-local gap remaining is **nuget.rs:93** — the `entry.file_name().to_str()` None branch in `has_signed_marker`. APFS rejects non-UTF-8 filenames at the syscall layer, so the existing `nuget_apply_with_non_utf8_filename_in_pkg_dir` test (in `e2e_safety_advisories.rs`) gracefully skips on macOS and fires on Linux runners. Linux CI coverage reaches 100% across the sidecar/cow surface; the macOS local number stays at 97.6% for this filesystem-capability reason alone. Workspace test sweep: green under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * test(e2e): cover cow.rs symlink/hardlink/stage-write error arms Four new direct-dispatch tests in e2e_safety_internals.rs that exercise cow.rs's `?` propagation arms via the pub `break_hardlink_if_needed` API. Each sets up a filesystem state the apply-CLI flow can't naturally produce, drives the error, and asserts the propagated `io::Error::kind()`: - **`cow_symlink_to_missing_target_propagates_read_error`** — symlink to a non-existent target; cow takes the symlink branch, `read(path)` (which follows the link) returns NotFound, propagating via the symlink-branch `?` arm. Covers cow.rs:66. - **`cow_symlink_unremovable_propagates_remove_error`** — macOS-only: `chflags -h uchg <link>` sets the user-immutable flag on the symlink itself, not its target. `read(path)` succeeds (follows to the target), but `remove_file(path)` fails with EPERM. Covers cow.rs:70. - **`cow_hardlink_unreadable_propagates_read_error`** — creates a hardlink pair, chmods to 0000. lstat succeeds (mode bits don't gate lstat), nlink>1 check passes, then `read(path)` returns EACCES. Covers cow.rs:84. Skipped under uid 0 (root bypasses mode bits). - **`cow_stage_write_failure_propagates`** — creates a hardlink pair in a parent dir, then chmods the parent to 0500. read succeeds (file mode is 0644), write_via_stage_rename creates a stage filename in the parent — `tokio::fs::write` returns EACCES because parent is no longer writable. Covers cow.rs:111. Skipped under uid 0. Coverage delta on `patch/cow.rs` regions: 88.89% → 93.83%. The remaining 5 regions are: - **cow.rs:71** — `write_via_stage_rename(path,target_bytes).await?` in the symlink branch. Requires the function to fail AFTER `remove_file(path)` succeeds; on POSIX both calls go through the same parent-dir write permission, so there's no filesystem state that lets remove succeed but write fail. - **cow.rs:97, 105** — `.unwrap_or_else` defaults on `path.parent()` and `path.file_name()`. Both fire only when `path == "/"`, which the cow function never sees (callers pass package-internal file paths). - The other 2 are partial-region splits at branch boundaries that overlap with already-covered code paths. Workspace test sweep: green under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * refactor(sidecars,cow): collapse two dead-arm Result paths Two small production simplifications that eliminate genuinely- unreachable error plumbing while leaving function contracts unchanged. Each strips a defensive-but-dead `.unwrap_or_else` / streaming-loop pattern down to the single-`?` shape the integration test suite can actually exercise. ## `cow.rs::write_via_stage_rename` The previous code used `.unwrap_or_else(|| Path::new("."))` and `.unwrap_or_else(|| "anon".to_string())` as fallbacks for the case where `path.parent()` or `path.file_name()` returned None. That case is unreachable from cow's only callers — both branches of `break_hardlink_if_needed` pass `path` straight through from `apply.rs`, which always builds it as `pkg_path.join(<file>)` (a real, two-segment package-internal path). The defaults were documentation, not behavior. Replaced with `.expect("…")` that documents the precondition inline. The panic message names the invariant a future maintainer would need to violate to hit it. No behavior change for any existing caller. ## `cargo.rs::sha256_file` The streaming `loop { file.read(&mut buf).await?; … }` pattern was defensive against large vendored sources, but the `.cargo-checksum.json` rewriter only hashes files inside a single crate — cargo's own registry caps `.crate` tarballs near 10MB unpacked. A single `tokio::fs::read(path).await?` is both simpler and collapses open + read into one `?` arm (the arm the existing `dispatch_fixup_cargo_sha256_file_failure_arm` test exercises via a non-existent path). The loop's per-chunk `?` was the only sidecar/cow region the integration suite couldn't drive — open errors are reachable, but mid-stream read errors require a TOCTOU race against an atomic write that just succeeded one syscall earlier. ## Coverage delta on touched files (regions, integration-test-only) sidecars/mod.rs 100.0% → 100.0% (unchanged) sidecars/cargo.rs 99.1% → 100.0% sidecars/nuget.rs 98.3% → 98.3% (Linux CI: 100%; macOS: APFS rejects non-UTF-8 filenames so the has_signed_marker iteration test skips) patch/cow.rs 93.8% → 98.7% (1 region remains: write_via_stage_rename `?` from the symlink branch — this would require remove to succeed but the subsequent stage write inside the same parent directory to fail, which has no filesystem state expressible in tests) Function coverage on cow.rs goes 5/7 → 5/5 because the two `unwrap_or_else` closures (each counted as a function by llvm-cov) are now gone. Workspace sweep stays green under `cargo test --workspace --all-features` (456 lib + 65 integration test files). Assisted-by: Claude Code:claude-opus-4-7 * refactor(nuget,cow): byte-suffix match + ACL test → 100% region cov Two final pushes to close the last uncovered regions in the sidecars/cow surface from any integration test runner. ## `sidecars/nuget.rs::has_signed_marker` The previous body wrapped the `.nupkg.sha512` check in `if let Some(name) = entry.file_name().to_str() { ... }`, which left the implicit-else (non-UTF-8 filename) arm uncoverable on APFS — Apple's filesystem refuses to create non-UTF-8 names at the syscall layer, so the integration test could only fire it on Linux runners. Rewrote against `entry.file_name().as_encoded_bytes()` and `ends_with(b".nupkg.sha512")`. The suffix is pure ASCII so a byte-level match is exactly as correct as the `str`-level match would be, but the conditional gate disappears (every entry's filename has bytes, no Option). Side benefit: a non-UTF-8 file that legitimately ends in `.nupkg.sha512` (e.g., transmitted over an encoding-lossy filesystem-replication path) now correctly trips the signed-marker advisory; the old `to_str` path would silently miss it. ## `cow.rs` symlink-branch `write_via_stage_rename` `?` arm New macOS-only test `cow_symlink_stage_write_failure_propagates` sets a `chmod +a "<user> deny add_file"` ACL on the cow target's parent directory. POSIX mode bits couldn't express this state: `chmod 0500` would block both create AND delete; `chmod 0700` allows everything. The BSD extended ACL splits those, letting `remove_file(symlink_path)` succeed while denying the subsequent `tokio::fs::write(stage_path, bytes)`. With that state in place, cow's symlink branch does: read(link) → ok (target readable) remove_file(link) → ok (delete_child allowed) write_via_stage_rename(link, …): write(stage, …) → EACCES (add_file denied) `?` propagates ← this is cow.rs:71 That's the last region the e2e suite couldn't reach. Skipped under uid 0 (root bypasses ACL deny entries). ## Final integration-test region coverage (macOS local) sidecars/mod.rs 100.0% sidecars/cargo.rs 100.0% sidecars/nuget.rs 100.0% patch/cow.rs 100.0% Workspace test sweep: 456 lib + 65 integration test files, zero failures under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): remove dead manifest::recovery + fuzzy_match exports Two unused chunks of code that nothing reaches (no callers anywhere in the workspace, no integration test exercises them): - **`crates/socket-patch-core/src/manifest/recovery.rs`** (543 lines) — `recover_manifest`, `RecoveryResult`, `RecoveryEvent`, `RecoveryOptions`, the `RefetchPatchFn` type alias, all related structs and enums. `git grep` returns zero callers; the module was wired up in `manifest/mod.rs` but nothing imported it. Likely a stalled design experiment. Drop the file + the `pub mod` declaration. - **`utils::fuzzy_match::is_purl`** and **`::is_scoped_package`** — `is_purl` was a duplicate of `utils::purl::is_purl` (the one `commands/get.rs` actually uses). `is_scoped_package` had no callers anywhere. Dropped both + their unit tests. - **`utils::fuzzy_match::MatchType`** downgraded from `pub` to private. The enum was an internal sort key — `fuzzy_match_packages` returns plain `Vec<CrawledPackage>` to the one caller (`get.rs:921`), so the tag was never visible across the module boundary. Net: 543 + ~20 lines of unreachable code removed, no behavior change. Workspace test sweep stays green (`cargo test --workspace --all-features`). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): purge dead utils::purl exports + duplicated tests Twelve `pub fn` exports in `utils/purl.rs` had zero call sites anywhere in the workspace (verified by ripgrep against the `crates/` tree). Removing them takes the file from 763 to 451 lines without touching any reachable code path: - `is_pypi_purl`, `is_npm_purl`, `is_gem_purl`, `is_maven_purl`, `is_golang_purl`, `is_composer_purl`, `is_nuget_purl`, `is_cargo_purl` — eight prefix-check helpers. Production code uses `Ecosystem::from_purl` (in `crawlers/types.rs`), which already does this dispatch with a proper enum return. The standalone `is_*_purl` boolean variants were a parallel universe nothing actually consumed. - `parse_npm_purl` — never called outside its own unit test. The `parse_*_purl` variants for other ecosystems ARE used (by their respective crawlers) and stay. - `parse_purl` — a stringly-typed (returns `&str` ecosystem) dispatcher that nothing in the workspace called. Each crawler uses the typed `parse_<eco>_purl` directly. - `build_pypi_purl` — no callers anywhere. (`build_npm_purl`, `build_gem_purl`, etc. ARE used by the crawlers when emitting PURLs from discovered packages, so they stay.) Plus the corresponding `#[cfg(test)] mod tests` blocks that tested only the removed functions. 312 lines of dead-export plumbing gone. Workspace sweep stays green under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): purge dead envelope builders + summary byte counters Three dead-by-disuse chunks in `socket-patch-cli/src/json_envelope.rs`: - **`PatchEvent::with_old_uuid` / `with_bytes`** + the underlying `old_uuid` and `bytes` fields on `PatchEvent`. Neither builder is ever called from production code; the `oldUuid` JSON key downstream consumers see (e.g. scan's update events) is emitted via direct `serde_json::json!` macros in `commands/get.rs` and `commands/scan.rs`, not via `PatchEvent`. Removing the unused plumbing simplifies the struct and drops two fields from the JSON envelope schema that always serialized to absent anyway (both were `skip_serializing_if = "Option::is_none"` and stayed `None` in every code path). - **`Summary::bytes_downloaded` and `Summary::bytes_freed`** counters. Both were summed from `PatchEvent.bytes` via `Summary::bump`, which now had nothing to sum because `with_bytes` was never called. The fields always serialized as `0`. The actual byte-tracking surface lives elsewhere — `commands/scan.rs::GcSummary::bytesFreed` (from `utils/cleanup_blobs.rs`). The envelope counters were parallel dead code. - **`PatchAction::as_tag` and `Command::as_tag`**. Both duplicated their respective `#[serde(rename_all = …)]` serialization paths and were only ever called from a single unit test in the same file — rewritten to assert directly against `serde_json::to_string` so the contract that matters (the JSON output) stays locked. `Summary::bump` shrank from `(action, bytes)` to `(action)`. Workspace test sweep stays green under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * test(core): integration coverage for diff + package + fuzzy_match Three new `crates/socket-patch-core/tests/` files lifting the previously-0%-from-integration files to full e2e coverage: - **`diff_e2e.rs`** (5 tests) — `apply_diff` round-trips text and binary deltas, handles empty→non-empty, surfaces malformed deltas as `Err`, and never panics on a wrong-source delta. Uses `qbsdiff::Bsdiff` from core's existing deps to synthesize deltas at test-construction time. - **`package_e2e.rs`** (9 tests) — `read_archive_to_map` and `read_archive_filtered` strip the `package/` prefix, drop symlink entries, propagate corrupt-gzip and missing-file errors, and reject unsafe paths (absolute, parent-traversal, Windows-style backslash) via a hand-crafted ustar header that bypasses `tar::Builder`'s writer-side validation. `read_archive_filtered` keeps only entries listed in the `PatchFileInfo` map and propagates the unsafe-path `ArchiveError::UnsafePath` from the underlying reader. - **`fuzzy_match_e2e.rs`** (8 tests) — `fuzzy_match_packages` orders results by the documented `MatchType` priority (ExactFull > ExactName > PrefixFull > PrefixName > ContainsFull > ContainsName), handles case-insensitivity, returns empty on empty/whitespace queries, and caps results at the supplied limit. Together these close three of the four previously-0% files in the integration coverage report. The fourth, `manifest/recovery.rs`, was deleted outright as dead code in commit 4e2f3a1. Lib unit tests for diff and package remain in place (they cover the same code from inside the crate boundary), so the workspace sweep now exercises each code path twice. Acceptable redundancy for the headline coverage gain. Workspace test sweep: green under `cargo test --workspace --all-features`. Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): remove dead Ecosystem::purl_prefix + manifest helpers Three more dead-export chunks identified by ripgrep audits: - **`Ecosystem::purl_prefix`** in `crawlers/types.rs` — five internal callers, all inside the unit-test module. Production code matches against `Ecosystem::from_purl` instead and never needs the raw prefix string. Removed the method + the per-ecosystem assertion against `.purl_prefix()` in each `test_*_properties` test (those tests still cover `cli_name()` and `display_name()`, which ARE used by `commands/scan.rs`). - **`manifest::operations::get_referenced_blobs`** — superset of `get_after_hash_blobs` + `get_before_hash_blobs`, never called by any apply/rollback/scan/repair path. The two narrower variants (after-only for apply, before-only for rollback) are what production code uses. - **`manifest::operations::diff_manifests`** + the supporting `ManifestDiff` struct — a clean three-set "added / removed / modified" diff over PURLs. Zero callers anywhere in the workspace. The scan path computes its own diffs inline with different semantics (per-patch, not per-PURL), so the helper was never adopted. Plus the corresponding unit tests for each removed export. Workspace test sweep stays green (118 + 419 lib tests). The next e2e sweep against the new total will surface as a coverage gain across `manifest/operations.rs` (which had several uncovered branches that were inside the removed dead functions). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): remove test-only pub helpers (nuspec parser, multi-update) Two more pub items with no production callers — only their own inline unit tests referenced them: - **`crawlers/nuget_crawler::parse_nuspec_id_version`** + `extract_xml_element` — a `.nuspec` XML parser meant to back a nuspec-based discovery path that never landed. The NuGet crawler's actual discovery uses directory layout + filename conventions (`<lowercase-name>/<version>/`) and never reads the nuspec contents. Both functions dropped along with their three test cases. - **`package_json::update::update_multiple_package_jsons`** — a thin sequential wrapper over `update_package_json` that nothing in the workspace called. The setup command iterates workspace package.json files itself; this convenience never found a caller. Workspace test sweep stays green (118 + 415 lib tests). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): drop duplicate utils::purl::build_npm_purl `utils::purl::build_npm_purl` was a byte-identical duplicate of `crawlers::npm_crawler::build_npm_purl`. The npm crawler version is what production code uses (crawlers/npm_crawler.rs:309 and :656 in the discovery loops); nothing imported the utils one. Removed the utils duplicate + its test. The npm-crawler version keeps its own tests. Workspace test sweep stays green (118 + 414 lib tests). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): drop dead utils::env_compat::read_env_either Identical re-export of `read_env_with_legacy` with no callers anywhere. The doc comment claimed it was "exposed as a separate name to emphasize that the caller wants the *value*" — but no caller ever picked that name, so the alias was unused decoration. Workspace test sweep stays green (118 + 414 lib tests). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): remove 4 unused .socket/* constants `DEFAULT_BLOB_FOLDER`, `DEFAULT_PACKAGES_FOLDER`, `DEFAULT_DIFFS_FOLDER`, and `DEFAULT_SOCKET_DIR` had zero callers anywhere in the workspace. The paths they encoded (`.socket/blob`, `.socket/packages`, `.socket/diffs`, `.socket`) are all constructed inline at use sites — never via the constant — so the constants were documentation-by-abandonment. `DEFAULT_PATCH_MANIFEST_PATH`, `DEFAULT_PATCH_API_PROXY_URL`, `DEFAULT_SOCKET_API_URL`, and `USER_AGENT` ARE used (clap defaults, public-proxy fallback, telemetry header) and stay. Workspace test sweep stays green (118 + 414 lib tests). Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): drop dead telemetry::track_patch_event_fire_and_forget Spawned a background tokio task to send a telemetry event without blocking the caller. Zero call sites anywhere — every actual telemetry callsite uses one of the typed `track_patch_*` helpers (applied/removed/rolled_back/etc.) which awaits the request directly. The fire-and-forget variant was unused infrastructure. Workspace test sweep stays green (118 + 414 lib tests). Assisted-by: Claude Code:claude-opus-4-7 * test(core): integration coverage for rollback new-file + error paths New `rollback_new_file_e2e.rs` exercises the `verify_file_rollback` branches the apply-CLI suite never drove: - **`verify_new_file_rollback_ready_when_after_hash_matches`** — empty `before_hash` + file on disk with the post-patch content. Rollback = delete, so the function reports `Ready`. Covers the `if is_new_file { ... Ready }` arm. - **`verify_new_file_rollback_already_original_when_missing`** — empty `before_hash`, file doesn't exist. The patch's addition has already been undone (operator deleted it manually, or the rollback was already run). Reports `AlreadyOriginal` so the rollback path can short-circuit. - **`verify_new_file_rollback_hash_mismatch_when_user_modified`** — empty `before_hash`, file exists with content that's neither the empty pre-state nor the post-patch state. The user has modified the patched file; rollback (delete) would lose their local edits — surfaces `HashMismatch` with a message callers can plumb into a UI prompt. - **`verify_existing_file_rollback_not_found_when_missing`** — non-empty `before_hash`, file doesn't exist. Reports `NotFound`. Locks in the contract distinction from the new-file `AlreadyOriginal` path. - **`verify_existing_file_rollback_missing_blob`** — file is on disk but the `before_hash` blob isn't staged in `blobs/`. Rollback can't synthesize the original content; reports `MissingBlob`. Workspace test sweep stays green. Assisted-by: Claude Code:claude-opus-4-7 * test(core): integration coverage for blob_fetcher early-return paths `blob_fetcher_edges_e2e.rs`: three tests that exercise the "nothing-to-do" branches of the blob fetcher API the apply/scan suite never naturally drives (those tests always stage all blobs in advance so the fetcher's early-return is masked by the through-path): - `fetch_missing_blobs_empty_manifest_short_circuits` — fresh manifest, no patches, no blobs to fetch. - `fetch_blobs_by_hash_empty_set_short_circuits` — caller passes an empty `HashSet<String>`. - `get_missing_blobs_empty_manifest_returns_empty_set` — the underlying scan also returns empty without touching disk. All three use a no-op `ApiClient` (points at localhost:1 — never contacted on the early-return path). Workspace test sweep stays green. Assisted-by: Claude Code:claude-opus-4-7 * chore(cleanup): silence test-only warnings (unused fixtures + stray attrs) Three small leftovers from prior cleanups: - **`utils/purl.rs`**: stray `#[cfg(feature = "maven")] #[test]` duplicated immediately above the golang test — leftover from the maven dead-test removal in commit b7c4cca. Deleted. - **`tests/in_process_python_envs.rs`**: helper `git_sha256` + its `sha2` / `Sha256` imports went unused after earlier test fixture refactors. Removed. - **`tests/in_process_remove_repair_lifecycle.rs`**: two `after_hash` test-fixture values that the surrounding mocks no longer reference. Prefixed with `_` so the reader still sees the intended fixture value. - **`tests/apply_network.rs`**: a `let mut args = vec![...]; let _ = args;` leftover from removing the apply-takes-api-flags path. Replaced with just the `argv` build the rest of the function actually uses. Build is now warning-clean under `cargo build --workspace --all-features --tests`. No behavior change. Assisted-by: Claude Code:claude-opus-4-7 * test(repair): cover --offline + --download-only mutual exclusion Two new tests in `repair_invariants.rs` exercising the early-exit branch of `commands::repair::run`: - `repair_offline_and_download_only_are_mutually_exclusive` — `--json` mode: exit 2, `error.code = invalid_args`, message mentions "mutually exclusive". - `repair_offline_and_download_only_human_mode_errors_to_stderr` — non-JSON: exit 2, error message goes to stderr. Covers `commands/repair.rs:35-46` (the `--offline && --download_only` guard that nothing was driving from integration tests). Assisted-by: Claude Code:claude-opus-4-7 * test(apply): cover no-.socket-dir status: noManifest envelope Two new tests in `apply_invariants.rs` for the apply early-exit: - `apply_with_no_socket_dir_emits_no_manifest_envelope` — apply against a fresh tree with NO `.socket/` directory emits `status: "noManifest"` in JSON mode and exits 0. - `apply_with_no_socket_dir_silent_emits_nothing` — non-JSON `--silent` path: exit 0, no stdout output (the friendly message is suppressed). Covers `commands/apply.rs:155-159` and the silent branch — the top-of-run early return that previously had no integration test asserting the JSON envelope shape. Assisted-by: Claude Code:claude-opus-4-7 * test(get): cover UUID-by-UUID paid-required path on public proxy `get_uuid_paid_patch_via_public_proxy_emits_paid_required_envelope` in `get_invariants.rs`: mocks the public-proxy `/patch/view/<uuid>` endpoint to serve `tier: "paid"` and asserts the JSON envelope shape (`status: paid_required`, `found:1, downloaded:0, applied:0`, `patches[0].tier: "paid"`). The existing paid-required test covered the package-name search path; this one closes the UUID-fetch branch in `commands/get.rs:756-768` that was never driven. Assisted-by: Claude Code:claude-opus-4-7 * test(get): batch coverage for get.rs envelope shapes Seven tests in new covering get.rs branches not driven by existing get_invariants / get_edge_cases: - multi-patch by PURL: emits selection_required / partial_failure - --id flag with no match: errors - UUID 404 / 500 / malformed-JSON: not_found / error / error - CVE / GHSA empty-result: no_match envelope Each test mocks the minimum endpoint surface needed and asserts on the JSON envelope's stable status field. Assisted-by: Claude Code:claude-opus-4-7 * test(cli): batch --dry-run + empty-manifest path coverage Six new tests in cli_dry_run_paths_e2e.rs covering --dry-run flag propagation and empty-manifest early-return envelopes: apply, repair, rollback, remove, list. Plus apply --silent suppresses friendly message check. Assisted-by: Claude Code:claude-opus-4-7 * test(output): integration coverage for ANSI color helpers Ten tests in output_helpers_e2e.rs driving format_severity and color directly via the lib's pub API. Existing integration tests all use --json mode which suppresses the colour wrappers, so the ANSI 31m/91m/33m/36m branches were entirely uncovered. Assisted-by: Claude Code:claude-opus-4-7 * test(blob_fetcher): cover fetch_blobs_by_hash skip-existing branch Pre-stage a blob and verify fetch_blobs_by_hash short-circuits the network call, reporting skipped:1. Assisted-by: Claude Code:claude-opus-4-7 * test(blob_fetcher): expand to 9 tests covering DownloadMode + sources Added 5 more tests: get_missing_archives empty, fetch_missing_sources in package/diff modes with no path configured, DownloadMode::parse across all variants (incl. 'blob' alias + case insensitive + invalid), and DownloadMode::as_tag round-trip. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): empty/missing path early-returns for NpmCrawler Three tests covering find_by_purls with empty PURL list, nonexistent node_modules, and crawl_all with no packages installed. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): empty-purl/empty-path branches across all 7 ecosystems Expanded crawlers_empty_paths_e2e.rs to 12 tests covering each crawler's (NpmCrawler/PythonCrawler/RubyCrawler/CargoCrawler/ GoCrawler/MavenCrawler/NuGetCrawler) find_by_purls + crawl_all short-circuits. Assisted-by: Claude Code:claude-opus-4-7 * test(telemetry): integration coverage for is_telemetry_disabled + sanitize_error_message Six tests in telemetry_helpers_e2e.rs: - 4 env-var combos for is_telemetry_disabled (=1, =true, VITEST=true, legacy var) - sanitize_error_message with + without home dir in input Also added serial_test as a dev-dep of socket-patch-core to serialize the env-var mutating tests. Assisted-by: Claude Code:claude-opus-4-7 * refactor(crawlers): runtime cfg!() to compile-time #[cfg(...)] gates Converts 9 runtime platform checks in production code to compile-time #[cfg(...)] gates so non-target-platform code drops out of the binary entirely. Affects: - python_crawler.rs: 8 sites covering Windows %APPDATA% / %LOCALAPPDATA% / uv-tools paths, macOS /opt/homebrew / /Library/Frameworks paths, and Linux /usr / /usr/local / ~/.local paths. - npm_crawler.rs: 1 site covering macOS Homebrew / nvm / volta / fnm fallback discovery. Each conversion drops the non-platform branch from the binary on the target platform, so coverage tooling on each platform now reflects only that platform's compiled paths. Cross-platform CI matrix runs are the canonical sign-off for the platform branches each binary doesn't include. This is a behavior-preserving refactor: cfg!() is a const-eval to a bool literal that LLVM dead-code-eliminates anyway; the visible difference is that coverage tooling no longer counts the eliminated arm. Workspace lib tests still green: 118 cli + 413 core. Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/python): 14 integration tests for find_python_dirs + venv + metadata New `crawler_python_e2e.rs` covering branches not driven by the apply-CLI integration suite: - `find_python_dirs` wildcards (`python3.*`, `*`, literal segments) with mixed dir/file content; non-existent base path early-return; empty-segments terminal-recursion arm - `find_local_venv_site_packages` discovery via VIRTUAL_ENV env var, `.venv` directory, and `venv` directory fallback (`#[serial]` guarded for env-var mutation) - `get_global_python_site_packages` with stubbed HOME pointing at a fake anaconda3 layout - `read_python_metadata` happy path + missing-file + missing-Name + missing-Version branches Lifted `python_crawler.rs` integration-test regions from 86.3% to 90.8%. Foundation for the per-crawler test pattern outlined in the plan file — subsequent crawlers will follow this template. Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/nuget): 15 integration tests for find_by_purls + crawl_all + paths New `crawler_nuget_e2e.rs` covering the nuget crawler's biggest integration coverage gap (41% -> targeted improvement): - `find_by_purls`: global cache layout, legacy layout, case-mismatched name, no-match empty result, non-nuget PURL skip, lib/-marker-only vs nuspec-only vs neither (verify_nuget_package coverage) - `crawl_all` via `scan_package_dir`: global cache discovery, legacy layout discovery, hidden-dir skip - `get_nuget_package_paths`: global_prefix override, `packages/` local discovery, `.csproj` triggers global fallback, `.sln` triggers global fallback, non-.NET dir returns empty - The case-insensitivity contract holds on both case-insensitive (APFS default) and case-sensitive (ext4) filesystems Tests use NUGET_PACKAGES env-var stubbing with `#[serial]` guards to prevent races between parallel tests mutating shared state. Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/ruby): 13 integration tests for find_by_purls + get_gem_paths New `crawler_ruby_e2e.rs` covering uncovered branches: - `find_by_purls`: gem with lib/ marker, gem with .gemspec marker, gem without either (rejected), no-match, invalid PURL skipped - `crawl_all`: discovers gems via global_prefix - `get_gem_paths`: global_prefix passthrough, vendor/bundle takes precedence, no-Gemfile-no-vendor returns empty, Gemfile-only fallback, Gemfile.lock-only fallback - Global discovery via `~/.gem/ruby/*/gems` (stubbed HOME) and `~/.rbenv/versions/*/lib/ruby/gems/*/gems` rbenv layout Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/maven): 16 integration tests for parse_pom + find_by_purls + repo paths New `crawler_maven_e2e.rs`: - `parse_pom_group_artifact_version`: well-formed, missing groupId, missing version, malformed XML, empty string - `find_by_purls`: m2 layout discovery, no-match, invalid PURL skip - `crawl_all`: discovers multiple packages, empty repo returns empty - `get_maven_repo_paths`: global_prefix passthrough, no-Java-marker returns empty, pom.xml / build.gradle / build.gradle.kts triggers repo discovery, M2_HOME/repository fallback when MAVEN_REPO_LOCAL unset Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/composer): 12 integration tests for vendor + installed.json paths New `crawler_composer_e2e.rs`: - `find_by_purls`: vendor with installed.json discovery, no installed.json returns empty, invalid PURL skip, version mismatch skip - `crawl_all`: installed.json parsing happy path, corrupt JSON returns empty - `get_vendor_paths`: global_prefix passthrough, no vendor returns empty, vendor without installed.json returns empty, vendor + installed.json but no composer.json/lock returns empty, full setup with composer.json returns vendor, full setup with composer.lock also works Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/cargo): 14 integration tests for parse_cargo_toml + find_by_purls + paths New crawler_cargo_e2e.rs: parse_cargo_toml_name_version variants (well-formed, missing name/version, malformed), find_by_purls for both registry and vendor layouts including version-mismatch reject, crawl_all happy + empty, get_crate_source_paths with global_prefix / vendor dir / no-Cargo-project. Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/go): 14 integration tests for encode/decode/parse + paths New crawler_go_e2e.rs: - encode_module_path: uppercase becomes !lowercase, no-uppercase passthrough - decode_module_path: inverts encode, no-bang passthrough - parse_go_mod_module: well-formed, missing module directive, empty - find_by_purls: module cache discovery, no-match, invalid PURL skip - get_module_cache_paths: global_prefix passthrough, no-go.mod returns empty, go.mod with GOMODCACHE env, GOPATH/pkg/mod fallback when GOMODCACHE unset Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/cargo): +3 tests for parse_dir_name_version fallback Three more tests in crawler_cargo_e2e.rs covering the workspace- version fallback path: when Cargo.toml has `version.workspace = true` instead of a concrete `version =`, both crawl_all and verify_crate_at_path fall back to parsing the directory name. Also covers the "dir without Cargo.toml entirely" skip. Assisted-by: Claude Code:claude-opus-4-7 * test(crawler/npm): 17 integration tests for npm crawler New crawler_npm_e2e.rs: - parse_package_name: unscoped, scoped, @-only-no-slash edge - build_npm_purl: scoped and unscoped - read_package_json: well-formed, missing file, malformed, missing name, missing version - find_by_purls: unscoped, scoped, version-mismatch, invalid PURL - crawl_all: discovers unscoped + scoped, skips dirs without package.json, skips dirs with corrupt package.json Assisted-by: Claude Code:claude-opus-4-7 * chore(crawlers): drop dead NpmPkgManager::as_tag + extend coverage NpmPkgManager::as_tag() and its corresponding test were dead — apply.rs matches on the enum variants directly (NpmPkgManager::YarnBerryPnP / ::Pnpm) and the struct never derives Serialize, so the stringified tag was unreachable from any caller. While here, extract `parse_bun_bin_output` from `get_bun_global_prefix` so the path-derivation half of bun discovery is unit-testable without shelling out to a real `bun` binary, and add integration tests covering: * cargo: TOML parser stops at next section / ignores pre-package lines, Default impl, CARGO_HOME unset → $HOME/.cargo fallback * npm: parse_bun_bin_output happy path, empty stdout, root-only path Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): more npm + composer coverage * npm: extract `parse_yarn_dir_output` and `parse_pnpm_root_output` from their shell-out wrappers so the path-derivation logic is unit-testable without a real `yarn` / `pnpm` binary; add tests covering happy path + empty stdout for both parsers and for the previously-extracted `parse_bun_bin_output`. * npm: cover `read_package_json` empty-string branches, `NpmCrawler` construction, `get_node_modules_paths` global_prefix passthrough and global-mode-without-prefix, and `find_workspace_node_modules` recursion / skip-list behavior. * composer: cover `get_global_vendor_paths` via COMPOSER_HOME env var and the HOME/.composer + HOME/.config/composer platform fallbacks, plus `crawl_all` dedup across vendor paths. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): maven + nuget + ruby + go coverage * maven: parent <groupId> fallback when project has none, property reference (`${...}`) bail-out for each of groupId/artifactId/version, parent property-reference skip, HOME/.m2/repository fallback, has_pom_file rejection of version dirs containing only a .jar, and `Default` impl. * nuget: global mode discovers nuget_home with NUGET_PACKAGES set, empty result when home doesn't exist, NuGet.Config marker triggers global-cache fallback, project.assets.json discovery (root + one level deep), malformed and empty-packageFolders assets.json arms, and `Default` impl. * ruby: `~/.rvm/gems/<set>/gems` layout discovery, and `Default` impl. * go: `Default` impl, empty `module` directive returns None, quoted module path branch, trailing-`!` decode arm, find_by_purls when the module dir is missing, crawl_all over nested versioned dirs, and the cache/ metadata-dir skip arm. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): python + cargo coverage * python: PythonCrawler `Default`, `find_by_purls` canonicalized-name match, qualifier stripping, empty/missing/mismatched purls, `crawl_all` over staged .dist-info dirs (well-formed + corrupt METADATA), global_prefix passthrough, and the METADATA early-break arm at first blank line after headers. * cargo: `parse_cargo_toml_name_version` `version.workspace` bail-out test, `verify_crate_at_path` dir-name fallback rejection on name mismatch, hidden-dir skip in `scan_crate_source`, dedup on identical purls across distinct directories, and local-mode fallback through `get_registry_src_paths` with CARGO_HOME stubbed (both with and without a staged registry/src tree). Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): deeper npm scope/nested + CrawlerOptions default * npm: a single staged tree that drives scoped-package scanning (`scan_scoped_packages`), nested `node_modules` recursion (`scan_nested_node_modules`), scoped→nested→scoped recursion, and the hidden-subdir + file-entry skip arms in both scanners. Adds PURL parser coverage for trailing `?` qualifier stripping, missing `@` version separator, empty version, scoped PURL with no `/`, and scoped PURL with empty name after the slash. * types: cover `CrawlerOptions::default()` populating cwd / global / global_prefix / batch_size (types.rs:143-150) — apply-CLI tests always construct options explicitly, so the Default impl was un-exercised. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): maven + go env-fallback coverage * maven: `get_maven_repo_paths(global=true)` with MAVEN_REPO_LOCAL set returns just that repo, and the empty-result arm when neither env var is set and HOME has no .m2/. * go: `get_gomodcache` falls through to `$HOME/go/pkg/mod` when both GOMODCACHE and GOPATH are unset (covers L194-197). Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): fix python METADATA blank-line break test The earlier fixture set BOTH Name and Version before reaching the blank line, so the function broke via the both-set guard at L71-72 instead of the blank-line break at L80-81. Replace with a fixture where only Name is set when the blank line is hit — that forces the L80-81 path and verifies the function correctly returns None when the trailer is interrupted before Version is read. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): npm shell-out wrappers via PATH stubbing Drive the `Command::new(...).output().ok()?` Err arm in each of the npm/yarn/pnpm/bun global-prefix helpers by stubbing PATH to a binary-free tempdir so the spawn itself fails. Removes the dependency on whether the dev host happens to have those binaries installed and covers the npm:91 / yarn:111 / pnpm:138 / bun:158 paths. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): composer/ruby/nuget shell-out + edge coverage * composer: cover `get_composer_home` falling through every source (COMPOSER_HOME unset, composer CLI missing from PATH, HOME without .composer or .config/composer) — drives the L194-207 shell-out failure path and the final L226 `None` arm. * ruby: similar PATH-stub for local Gemfile + missing `gem` binary (run_gem_env Err arm), plus global-mode probe with no gem binary and no HOME-relative gem layouts (covers fallback_globs scanning branches). * nuget: cover scan_package_dir's "skip non-dir entries" arm via a plain file at the top of the package dir, and the read_dir Err short-circuit via a non-existent global_prefix. Assisted-by: Claude Code:claude-opus-4-7 * test(crawlers): maven + cargo final coverage * maven: cover the `artifact_id?` propagation arm when a POM has groupId+version but no artifactId, and the `extract_xml_value` same-line-close-tag guard when an XML element is split across lines. * cargo: cover `scan_crate_source`'s non-dir entry skip arm (plain file at top of source path), the parse_dir_name_version fallback in `read_crate_cargo_toml` when Cargo.toml is unparseable AND the dir name has no version, and the `verify_crate_at_path` false-on- both-pars…
Configuration menu - View commit details
-
Copy full SHA for b8de84f - Browse repository at this point
Copy the full SHA b8de84fView commit details
Commits on May 26, 2026
-
feat(vex): OpenVEX 0.2.0 attestation generator (#81)
* feat(vex): OpenVEX 0.2.0 attestation generator Adds a new `socket-patch vex` subcommand that emits an OpenVEX 0.2.0 document derived from the local manifest. Each statement reports the top-level product as `not_affected` by a given vulnerability with justification `inline_mitigations_already_exist`, pointing at the patched dependency as a subcomponent. Design highlights: - New self-contained module `socket-patch-core::vex` (schema, build, product detection, on-disk verify, RFC 3339 time). No openvex crate dependency — those are unmaintained. - One statement per vulnerability ID. GHSA becomes `name`, CVEs become `aliases`. Two patches that fix the same GHSA merge their subcomponents into one statement. - Auto-detects the product PURL from package.json > pyproject.toml > Cargo.toml. `--product` overrides. - On-disk hash verification is the default; `--no-verify` skips it. Patches that fail verification are silently omitted from the document (never emitted as `affected` or `under_investigation`) and surfaced as stderr warnings / `--json` envelope `skipped` events. - Exit 0 on success, 1 on no-applicable-patches, 2 on hard errors (manifest unreadable, `--json` without `--output`, etc.). CI installs Go + vexctl before the test step; `tests/e2e_vex.rs` runs `vexctl inspect` against the generated document whenever the binary is on PATH. Local devs without Go see a skip message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(vex): pin actions/setup-go to v6.4.0 SHA Org policy (and zizmor) require all action references to use a 40-char SHA pin. Resolved via `gh api repos/actions/setup-go/git/refs/tags/v6.4.0`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(vex): detect product from git remote origin first Adds git-remote-origin detection as the top-priority signal in vex::product::detect_product. The repo is usually the canonical product identifier when it's a git checkout; package.json / pyproject.toml / Cargo.toml become fallbacks. Format: - git@github.com:owner/repo.git → pkg:github/owner/repo - https://github.com/owner/repo → pkg:github/owner/repo - gitlab.com / bitbucket.org → pkg:gitlab|bitbucket/owner/repo - self-hosted / unknown forge → raw URL (OpenVEX @id accepts any URI) Walks ancestors of cwd looking for `.git/config`, so subdir invocations (common in monorepos) still resolve correctly. SSH (`git@...`), `ssh://`, `git://`, `git+ssh://`, `http(s)://` all parse to host+path. Also fixes the vexctl integration: `vexctl inspect` doesn't exist at v0.3.0; switched to `vexctl list` which loads and parses the document and exits non-zero on malformed input — the de facto schema gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(vex): use `vexctl merge` for round-trip validation `vexctl list <arg>` parses <arg> as a selector (status / justification), not a file path. Switch to `vexctl merge --files <path>` which loads, parses, and re-emits the document — the canonical single-file parse gate at vexctl v0.3.x. A successful merge proves the input is valid OpenVEX; we additionally assert the output round-trips through serde as JSON. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(vex): pass file path positionally to vexctl merge `vexctl merge` accepts file paths positionally, not via `--files`. Verified locally with vexctl v0.3.0: $ vexctl merge ./out.vex.json { ... merged document ... } Exit 0 with our generated doc confirms parse + schema validation against the OpenVEX 0.2.0 spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(vex): extensive unit + conformance tests, OpenVEX 0.2.0 schema completeness Drives line coverage of `socket-patch-core::vex` to ~99.7% (1846/1851 lines covered) and locks down OpenVEX 0.2.0 spec conformance. Schema additions (all `Option<T>`, all `skip_serializing_if = "Option::is_none"`, no change to what the builder emits today): - `Document.role`, `Document.last_updated` - `Statement.@id`, `Statement.last_updated`, `Statement.supplier`, `Statement.action_statement` - `Product.identifiers`, `Product.hashes` - `Subcomponent.identifiers`, `Subcomponent.hashes` Backwards-compatible: existing docs round-trip unchanged; new fields appear only when callers set them. Test count went from 33 to 144 across the vex module (+111 tests): - schema.rs: 6 → 24 — every Status/Justification variant, all new optional fields, missing-required-field rejection, version typing, multi-aliases ordering. - build.rs: 7 → 17 — applied PURL not in manifest, zero-vuln patch, empty CVE list, duplicate CVE dedup, tooling=None, empty author, determinism, timestamp consistency, subcomponent sort order. - product.rs: 22 → 53 — `[tool.poetry]` fallback, CRLF git config, all URL scheme branches (git+ssh, git://, http://, port-suffix, no-user), no-origin/empty-url config fallbacks, multi-manifest combos beyond pkg+cargo, non-string JSON name/version, missing-version-key, parse_toml_kv negative cases, three-segment URL path, trailing-slash normalization. - verify.rs: 5 → 11 — empty manifest, zero-file patch (vacuous), extra package_paths ignored, multi-file short-circuit, Default/Clone/Eq impls. - time.rs: 5 → 15 — non-leap Feb, year-end boundary, century non-leap (2100), 400-year leap (2000), every month-length transition, u64::MAX no-panic. - mod.rs: 0 → 1 — re-export smoke test (compile-time guard). - conformance_tests.rs (new): 17 cross-cutting tests pinning OpenVEX spec rules — @context literal, JSON-LD @-prefixed keys, status/justification interaction (action_statement reserved for status=affected; not_affected requires justification), required-field presence, non-empty identifiers, timestamp consistency, version=1, no-null invariant, alias/subcomponent uniqueness. Remaining 5 uncovered regions documented in-source as unreachable in practice (e.g. `civil_from_days` negative-`z` arm — requires inputs past year ~292 billion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for 8ee38a3 - Browse repository at this point
Copy the full SHA 8ee38a3View commit details -
feat: telemetry coverage for read-side commands + paid-tier fallback …
…(3.1.0) (#82) * feat(telemetry): gate on SOCKET_OFFLINE for airgap compliance is_telemetry_disabled() now returns true when SOCKET_OFFLINE is "1" or "true". Airgap mode promises "never contact the network"; the telemetry endpoint is a network call, so honoring SOCKET_OFFLINE here keeps every command (apply, remove, rollback — plus future scan/get/etc.) compliant without requiring per-command gating. Adds three integration tests in telemetry_helpers_e2e.rs and extends the existing test_is_telemetry_disabled unit test with the new branch (including "0" and "" non-truthy values). Assisted-by: Claude Code:opus-4-7 * feat(telemetry): add event variants + trackers for read-side + housekeeping + vex Extends PatchTelemetryEventType with 12 new variants covering scan, get (emits patch_fetched / patch_fetch_failed for symmetry with the existing apply naming convention), list, repair, setup, unlock, and the new vex (OpenVEX) command. Adds matching convenience tracker functions that funnel through the existing track_patch_event send path — no new HTTP plumbing. The scan/get trackers carry a fallback_to_proxy flag so we can measure how often the auth endpoint downgrades to the public proxy once that fallback path lands. No call sites yet — wiring into each command file follows in subsequent commits so this commit stays a pure data-model addition. Assisted-by: Claude Code:opus-4-7 * feat(telemetry): wire patch_* events into scan, get, list, setup, repair, unlock, vex Each command now fires a success/failure event through the existing track_patch_event send path. Concrete coverage: - list: patch_listed (count surfaced) - setup: patch_setup (detected package manager: npm/pnpm) - unlock: patch_unlocked (was_held + released metadata) + patch_unlock_failed - repair: patch_repaired (downloaded + cleaned counts) + patch_repair_failed - scan: patch_scanned (per-tier counts, can_access_paid, ecosystems, fallback_to_proxy=false placeholder) + patch_scan_failed when every batch errored (previously hidden as "zero patches found") - get (UUID path only for now): patch_fetched on success, patch_fetch_failed on paid_required / not_found / API error. CVE/GHSA/PURL search-error paths also surface patch_fetch_failed. - vex: vex_generated on success, vex_failed via a small async helper that wraps each emit_envelope_error call site. Renamed the unlock tracker's "broken" parameter to "released" — unlock never breaks a held lock (that's `--break-lock` on mutating subcommands); the bool actually describes whether the lock file was removed. No new HTTP plumbing; trackers reuse track_patch_event. Behavior preserved on existing apply/remove/rollback paths. Assisted-by: Claude Code:opus-4-7 * test(scan): add lifecycle coverage for withdrawn patches and updates Three new cargo tests in scan_invariants.rs covering patch-management behaviors the existing matrix didn't pin down: - scan_prune_keeps_entry_when_package_installed_but_api_silent: a manifest entry must survive --prune when the underlying package is still installed locally but the API has fallen silent on patches for it. Pins the current --prune scope (crawl-absence, not API-absence) so a future regression to over-pruning is loud. - scan_prune_removes_withdrawn_patch_entry: when the underlying package is uninstalled (no longer in crawl results), --prune removes the manifest entry even with a stale blob still on disk. The blob is left for the existing repair-side GC to handle. - scan_detects_update_without_touching_existing_blobs: a newer UUID from the API surfaces in the `updates` array, but scan without --apply must leave the on-disk manifest and blobs byte-for-byte unchanged. Read-only invariant. Assisted-by: Claude Code:opus-4-7 * test(telemetry): end-to-end behavioral coverage for apply/scan/get + airgap New tests/telemetry_e2e.rs spawns the released binary against a wiremock server that fronts both the patches endpoints AND the telemetry endpoint, then counts POSTs against /v0/orgs/{slug}/telemetry filtered by event_type. Coverage: - scan_emits_patch_scanned_telemetry_on_success - list_emits_patch_listed_telemetry_when_telemetry_enabled - get_emits_patch_fetched_telemetry_on_uuid_lookup_success (tolerates either fetched/fetch_failed — the apply step is allowed to fail in the test env; the invariant is that *some* event fires) - {apply,scan,get,list}_skips_telemetry_in_airgap_mode — confirms the central is_telemetry_disabled() gate suppresses everything when SOCKET_OFFLINE=1, regardless of command. Caught a real test-only bug along the way: send_telemetry_event reads SOCKET_API_URL from the *environment*, not from the clap --api-url arg. The test harness now sets both env + flag so the telemetry POST lands on the same mock recording the API requests. Assisted-by: Claude Code:opus-4-7 * feat(api): auth → proxy fallback on 401/403 for scan + get Adds `build_proxy_fallback_client(&overrides)` + `is_fallback_candidate(&err)` in api/client.rs. The constructor builds a public-proxy-mode ApiClient from the same overrides used by `get_api_client_with_overrides`, deliberately dropping the auth token. The classifier flags 401/403 errors as fallback-eligible; everything else (404, 5xx, network, rate-limit, parse) surfaces unchanged. `scan.rs` and `get.rs` (UUID path) catch the first such error from the authenticated endpoint, log a warning to stderr, rebuild the client, retry the same request once, and continue. A new `fallback_to_proxy` bool plumbed through to the existing telemetry trackers carries the incidence into observability. Behavior is deliberately conservative: - Read commands only — `apply`/`remove`/`rollback`/`vex` keep their pre-existing fail-loud-on-auth semantics. - 404, 5xx, network, parse errors do NOT trigger fallback; they surface as before so backend issues stay visible. - Free patches still resolve via the proxy; paid patches return the same "paid_required" structured error the no-token path already emits. Assisted-by: Claude Code:opus-4-7 * test(fallback): cover auth → proxy downgrade and conservative classifier Two new tests in telemetry_e2e.rs: - scan_falls_back_to_proxy_on_401_and_tags_telemetry: stands up two mock servers (auth endpoint 401s, proxy endpoint succeeds), asserts scan exits 0 after the swap, the fallback warning hits stderr, and the resulting patch_scanned event carries fallback_to_proxy: true in metadata. - scan_does_not_fall_back_on_500: pins the conservative scope of the classifier. A 500 from the auth endpoint must NOT trigger the proxy retry — backend errors should stay visible. Asserts zero hits against the proxy mock and no fallback warning on stderr. Assisted-by: Claude Code:opus-4-7 * chore(release): bump to 3.1.0 Workspace Cargo.toml, all npm wrapper + per-platform packages, and PyPI pyproject.toml synced via scripts/version-sync.sh (with manual fixup for the per-platform packages since npm install couldn't process the workspace catalog: protocol). CHANGELOG entry covers: telemetry events across the read-side and housekeeping commands, the 401/403 auth → public-proxy fallback in scan/get, the SOCKET_OFFLINE airgap gate, and the new behavioral + lifecycle test coverage that backs all of it. Assisted-by: Claude Code:opus-4-7 * fix(clippy): allow too_many_arguments on track_patch_scanned cargo clippy --workspace --all-features -- -D warnings flagged track_patch_scanned at 8/7 args. Grouping the per-tier counts + ecosystems list + fallback flag + auth tuple into a struct would force every call site to build a config object for a single fire-and-forget tracker — worse ergonomics. Annotating the lint is the right call; `track_patch_event` already exists for callers that want full control. Assisted-by: Claude Code:opus-4-7
Configuration menu - View commit details
-
Copy full SHA for 1493421 - Browse repository at this point
Copy the full SHA 1493421View commit details
Commits on May 27, 2026
-
Configuration menu - View commit details
-
Copy full SHA for e3ab57d - Browse repository at this point
Copy the full SHA e3ab57dView commit details -
ci(release): tag after build + fix rustup toolchain setup (#85)
Tag now runs after build succeeds so a failed build no longer leaves behind a dangling tag. All publish jobs depend on tag, so we never half-publish without one. Replaces dtolnay/rust-toolchain (which can't auto-detect the channel when pinned by SHA) with direct rustup invocation, matching the pattern already used in ci.yml. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for dde67db - Browse repository at this point
Copy the full SHA dde67dbView commit details -
ci(release): pin newer cross NDK image for aarch64-linux-android (#86)
The default image bundled with cross 0.2.5 ships an NDK whose sysroot lacks libunwind on the linker path. Modern rustc emits `-lunwind` for Android targets, so the release build fails with `ld: cannot find -lunwind`. Override just the Android target to a digest-pinned cross main image, which ships an NDK that has libunwind available. All other targets keep their default cross 0.2.5 images. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Configuration menu - View commit details
-
Copy full SHA for c20619b - Browse repository at this point
Copy the full SHA c20619bView commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff 3a58178...c20619b