You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Harden vendored-mode trust in patch-service artifacts (#249)
* fix(vendor): refresh the ledger fingerprint when a wired gem/maven/nuget artifact is rebuilt
When the hot path found the committed artifact missing or stale, it rebuilt
it but returned no entry, so the ledger kept the previous fingerprint: the
gem file inventory, the maven/nuget sha256, and the nuget lock pin. A
rebuild from the other source (service vs local) then produced bytes the
ledger did not describe. VEX and verify reported tamper, repair of a
service-vendored maven/nuget entry could fail, --revert left
packages.lock.json pinned to the patched contentHash, and a service-sourced
rebuild was labelled already_vendored.
The rebuild branches now return a refreshed entry built from the new bytes
or tree, with no wiring of their own. For nuget the entry carries only the
re-pinned lock record, with original: None. carry_forward_wiring (same
uuid) re-attaches the first run's records and fills in the true
pre-vendor hash.
The CLI does not record such an entry when the ledger has no previous one,
because it would carry no wiring and --revert would delete the artifact
while the project still points at it. repair carries the repaired entry's
wiring forward before persisting, and emits vendor_inventory_refreshed when
the backend's refreshed inventory differs from the recorded one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): verify a served maven/nuget archive carries the patch before accepting it
service_archive_copy (the Tier-A maven .jar / nuget .nupkg path) accepted any
archive that matched its SRI. The bytes were written verbatim and every
patched file was reported AlreadyPatched, so a served archive without the
patch was committed as patched. The hot path then saw a stale artifact on
every later run and rebuilt it.
The helper now takes the PatchRecord and requires every patched member to
hash to its afterHash (zip_bytes_match_after_hashes, the check the hot path
already uses) before returning Used. A mismatch is a service miss: auto
falls back to the local build with vendor_prebuilt_layout_mismatch, and
service refuses with vendor_prebuilt_required. The vendor_prebuilt_downloaded
advisory is only pushed for accepted bytes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): refuse a prebuilt artifact that fails integrity under auto too
ServiceArtifact documents IntegrityMismatch as always a hard error, but
golang, composer, pypi, npm and gem (the .gem and the stub gemspec) mapped
it through their miss policy. Under auto they warned and built the package
locally, so a sign of tampering became a quiet fallback. Under service
they refused with the generic vendor_prebuilt_required code.
These arms now refuse in every mode, as cargo already did: golang,
composer, pypi and gem return vendor_prebuilt_integrity_mismatch, and npm
fails the package with the integrity detail. The npm test that pinned the
old fallback is replaced by one that expects the refusal. The service-mode
tests for these ecosystems now expect the specific code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): refuse --vendor-source=service when no API client is configured
Every backend's service helper treats !service_enabled() as "build
locally", and service_enabled() is false without a client. The
service-only policy was enforced by common::service_offline_conflict, which
checked only --offline. So a VendorServiceConfig with source: Service and
client: None built the artifact locally in every backend, contradicting
service's fail-closed promise. The CLI always passes a client, so only
library callers could reach this.
The gate every backend already calls at its entry point now also refuses
that combination (vendor_prebuilt_required), before any service
consultation or write. Regression tests cover all eight backends.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): route the cargo wired-copy rebuild through --vendor-source
The in-sync hot path rebuilt a missing/stale crate copy with
copy_and_patch directly, so `--vendor-source=service` with --offline or
no API client rebuilt locally and reported success, and online service
mode never consulted the patch service. Refuse via
service_offline_conflict first and prefer cargo_service_copy, falling
back to the local build only where the policy allows, as composer and
gem already do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): verify a served pypi wheel / npm tarball carries the patch
The service SRI proves only that the download is intact. A wheel or
tarball whose patched members still held the original bytes was written
as-is, reported as already patched, and pinned in the lockfile. Check
each patched member against its afterHash before using the artifact:
auto builds locally with vendor_prebuilt_layout_mismatch, service
refuses (pypi: vendor_prebuilt_required; npm fails the package). Adds
read_archive_bytes_to_map for the in-memory tarball check.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(vendor): only record a rebuilt entry over a same-uuid predecessor
A wiring-less refreshed entry relies on carry_forward_wiring, which
re-attaches wiring only from a same-uuid ledger entry. Over an entry
from another patch uuid (the run that wired this uuid never saved), the
guard let it through, saving an entry --revert could not unwire and
sweeping the other uuid's dir. Leave the ledger as it is instead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: crates/socket-patch-cli/CLI_CONTRACT.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -472,12 +472,14 @@ per service outcome:
472
472
| Service outcome |`auto`|`service`|
473
473
|---|---|---|
474
474
| granted/reused, integrity ok |**use service**|**use service**|
475
-
| integrity mismatch | cargo/maven/nuget: **refuse** (`vendor_prebuilt_integrity_mismatch`) — tampered bytes never fall back; other ecosystems (to be aligned): local build + `vendor_prebuilt_integrity_mismatch`| refuse (cargo/maven/nuget: `vendor_prebuilt_integrity_mismatch`; others: `vendor_prebuilt_required`) |
475
+
| integrity mismatch (including the gem stub gemspec) |**refuse** (`vendor_prebuilt_integrity_mismatch`; npm: the package fails with the integrity detail). Tampered bytes never fall back to a local build | refuse (same) |
476
+
| integrity ok, but the archive does not carry the patched files (a member at a recorded path fails its `afterHash`; checked for cargo/golang/composer/gem after extraction, and for maven/nuget/pypi/npm before the archive is written; npm under `service` fails the package with the detail) | local build + `vendor_prebuilt_layout_mismatch`| refuse (`vendor_prebuilt_required`) |
476
477
| still building (`pending_build` / serve 408) | local build + `vendor_prebuilt_pending`| refuse |
477
478
| not built / withdrawn / not found / no usable artifact | local build (quiet) | refuse |
| 401 / 403 grant / 5xx / network error | local build + `vendor_prebuilt_unavailable`| refuse |
480
481
|`--offline`| local build | refuse (`vendor_service_offline_conflict`) |
482
+
| no API client configured (library callers of the vendor engine; the CLI always configures one) | local build | refuse (`vendor_prebuilt_required`) |
481
483
482
484
**golang service leg staging (v5.0)**: the module zip is downloaded, extracted and `h1:`-verified in a `<copy>.socket-stage` sibling and swapped into place only afterwards; a failed re-download of a WIRED, present copy keeps the copy and its `replace` directive (previously both were torn down), while a missing copy still drops the dangling directive.
483
485
@@ -1123,7 +1125,7 @@ Every `--json` invocation emits a single JSON object that follows the **unified
1123
1125
|`vendor_fetch_unverifiable`|`skipped` (warning) | vendor: the lockfile records no usable integrity for the missing package; nothing was fetched (fail-closed) and the `package_not_installed` skip follows. |
1124
1126
|`vendor_artifact_missing`|`skipped` (warning) / `failed`| vendor: the committed artifact is gone — the registry resolution is recovered from the ledger and the artifact rebuilt (warning); repair `--offline` with no local source surfaces it as the per-entry failure instead. |
1125
1127
|`vendor_artifact_corrupt`|`failed`| repair `--offline`: the committed artifact fails verification (member afterHashes or the ledger's whole-file sha256) and no local source can rebuild it. Online repairs rebuild instead. |
1126
-
|`vendor_artifact_rebuilt`|`skipped` (warning) | vendor / scan `--vendor`: a wired-but-missing/stale artifact was rebuilt in place; lockfiles and the ledger entry untouched. (Under `repair` the `rebuilt` event carries this signal.) |
1128
+
|`vendor_artifact_rebuilt`|`skipped` (warning) | vendor / scan `--vendor`: a wired-but-missing/stale artifact was rebuilt in place. The lockfiles are untouched, except that nuget re-pins `packages.lock.json` to the rebuilt bytes. gem/maven/nuget: the package's event is `applied` (also for a rebuild from the patch service), and the ledger entry's artifact fingerprint (gem `fileInventory`, maven/nuget `sha256` + `size`, and the nuget lock pin) is refreshed to the rebuilt bytes, and its wiring records are kept unchanged, so `--revert` still restores the pre-vendor files. A rebuild whose ledger has no entry for the package, or only one from another patch uuid, records none. cargo/composer/gem rebuilds honour `--vendor-source` like a fresh vendor (`service` downloads the prebuilt artifact and refuses when it cannot). Other ecosystems leave the ledger entry untouched. (Under `repair` the `rebuilt` event carries this signal.) |
1127
1129
|`vendor_artifact_rebuild_failed`|`failed`| repair: the rebuild ran but the result failed verification against the recorded fingerprint (e.g. an edited state.json sha); the unverifiable artifact was removed. |
1128
1130
|`vendor_artifact_unrepairable`|`failed`| repair: no verifiable pristine source exists (not installed + lockfile rewired + no recoverable ledger fragment), the wheel is platform-locked with no installed copy, or the ledger entry itself cannot be trusted. |
1129
1131
|`vendor_uuid_mismatch`|`skipped`| repair: the manifest's patch uuid moved past the vendored artifact — a re-vendor (`vendor` / `scan --vendor`) is pending; repair does not cross patch generations. |
0 commit comments