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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 114 additions & 7 deletions crates/socket-patch-cli/src/commands/agent_download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use socket_patch_core::api::ranking::severity_order;
use socket_patch_core::api::types::{PatchResponse, PatchSearchResult, VulnerabilityResponse};
use socket_patch_core::crawlers::{CrawlerOptions, Ecosystem};
use socket_patch_core::manifest::operations::{read_manifest, write_manifest};
use socket_patch_core::manifest::records::{build_patch_record, files_for_manifest};
use socket_patch_core::manifest::records::{
build_patch_record, files_for_manifest, refresh_opt_in_metadata,
};
use socket_patch_core::manifest::schema::{PatchFileInfo, PatchManifest, PatchRecord};
use socket_patch_core::patch::apply::{is_valid_blob_hash, select_installed_variants_any};
use socket_patch_core::patch::apply_lock::{LockError, LockGuard};
Expand All @@ -22,7 +24,7 @@ use socket_patch_core::vendor::refusals::{
vendor_refusals, LedgerLoad, RefusalScope, VendorRefusal, VendorRefusals,
};
use socket_patch_core::vendor::{load_state, lookup_entry, VendorEntry, VendorState};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;

Expand Down Expand Up @@ -148,6 +150,12 @@ pub(crate) fn patch_event_metadata(patch: &PatchResponse) -> serde_json::Value {
serde_json::Value::String(patch.license.clone()),
);
meta.insert("tier".into(), serde_json::Value::String(patch.tier.clone()));
if let Some(requires_opt_in) = patch.requires_opt_in {
meta.insert("requiresOptIn".into(), serde_json::json!(requires_opt_in));
}
if let Some(opt_in_note) = &patch.opt_in_note {
meta.insert("optInNote".into(), serde_json::json!(opt_in_note));
}
meta.insert(
"exportedAt".into(),
serde_json::Value::String(patch.published_at.clone()),
Expand Down Expand Up @@ -356,6 +364,7 @@ pub struct DownloadParams {
pub global_prefix: Option<PathBuf>,
pub json: bool,
pub silent: bool,
pub offline: bool,
/// When `false` (the default — narrow), a release-variant package (PyPI
/// `?artifact_id=`, RubyGems `?platform=`, Maven `?classifier=`) is
/// filtered down to the variant(s) matching the locally-installed
Expand Down Expand Up @@ -695,6 +704,7 @@ pub(crate) struct FetchBatch {
failed: usize,
/// Fetched, recordable patches in selection order.
fetched: Vec<FetchedPatch>,
metadata_refreshes: Vec<PatchResponse>,
/// Ledger store only: `(purl, record)` reused from a detached entry
/// already at the selected uuid (no fetch).
reused: Vec<(String, PatchRecord)>,
Expand Down Expand Up @@ -843,7 +853,8 @@ pub(crate) fn claimed_purls(
}

/// The record a detached ledger entry already carries for `purl` at
/// exactly `uuid` — the ledger store's idempotency skip (no view fetch).
/// exactly `uuid`. Online reuse still fetches its view for authoritative
/// activation metadata; offline reuse keeps the existing fields.
/// Always `None` for the manifest store.
pub(crate) fn detached_ledger_record<'a>(
store: RecordStore<'a>,
Expand Down Expand Up @@ -906,6 +917,7 @@ pub(crate) async fn fetch_selected_patches(
already_recorded: 0,
failed: 0,
fetched: Vec::new(),
metadata_refreshes: Vec::new(),
reused: Vec::new(),
events: Vec::new(),
warnings,
Expand Down Expand Up @@ -962,8 +974,40 @@ pub(crate) async fn fetch_selected_patches(
}

// Idempotency (ledger store): a detached entry already at this uuid
// carries its own record — no view fetch needed.
if let Some(record) = detached_ledger_record(store, purl, uuid).cloned() {
// retains its artifact and wiring; its view still governs activation metadata.
if let Some(mut record) = detached_ledger_record(store, purl, uuid).cloned() {
if !params.offline {
let view = match prefetched.remove(uuid) {
Some(patch) => Ok(Some(patch)),
None => api_client.fetch_patch(uuid).await,
};
let patch = match view {
Ok(Some(patch)) => patch,
Ok(None) => {
batch.fail(
params.json,
Some(format!("[fail] {purl} (could not fetch details)")),
(purl, uuid),
DOWNLOAD_FAILED,
"could not fetch details",
);
continue;
}
Err(e) => {
batch.fail(
params.json,
Some(format!("[fail] {purl} ({e})")),
(purl, uuid),
DOWNLOAD_FAILED,
&e.to_string(),
);
continue;
}
};
record.requires_opt_in = patch.requires_opt_in;
record.opt_in_note = patch.opt_in_note.clone();
batch.metadata_refreshes.push(patch);
}
if !quiet {
eprintln!("{}", format_record_skip(purl, "already vendored"));
}
Expand Down Expand Up @@ -1033,13 +1077,15 @@ pub(crate) async fn fetch_selected_patches(
},
};
if action == PatchAction::Skipped {
batch.metadata_refreshes.push(patch.clone());
if !quiet {
eprintln!("{}", format_record_skip(&patch.purl, "already in manifest"));
}
batch.events.push(
PatchEvent::new(EventAction::Skipped, patch.purl.as_str())
.with_uuid(patch.uuid.as_str())
.with_reason(ALREADY_IN_MANIFEST, "already in manifest"),
.with_reason(ALREADY_IN_MANIFEST, "already in manifest")
.with_details(patch_event_metadata(&patch)),
);
batch.skipped += 1;
batch.already_recorded += 1;
Expand Down Expand Up @@ -1140,6 +1186,7 @@ pub(crate) async fn fetch_selected_patches(
/// envelope, how many patches failed, and the records the vendor step
/// consumes.
pub(crate) struct DetachedDownload {
authoritative: HashSet<String>,
pub(crate) code: i32,
pub(crate) events: Vec<PatchEvent>,
pub(crate) warnings: Vec<RunWarning>,
Expand All @@ -1155,6 +1202,23 @@ impl DetachedDownload {
self,
env: &mut Envelope,
) -> (i32, usize, HashMap<String, PatchRecord>) {
for (purl, record) in &self.records {
if self.authoritative.contains(&record.uuid) {
env.register_view_opt_in(
purl,
&record.uuid,
record.requires_opt_in,
record.opt_in_note.as_deref(),
);
} else {
env.register_opt_in(
purl,
&record.uuid,
record.requires_opt_in,
record.opt_in_note.as_deref(),
);
}
}
for event in self.events {
env.record(event);
}
Expand Down Expand Up @@ -1238,11 +1302,18 @@ pub(crate) async fn download_patch_records_preflighted(
)
.await;

let mut authoritative: HashSet<String> = batch
.metadata_refreshes
.iter()
.map(|patch| patch.uuid.clone())
.collect();
let mut records: HashMap<String, PatchRecord> = batch.reused.into_iter().collect();
for FetchedPatch { patch, files, .. } in batch.fetched {
authoritative.insert(patch.uuid.clone());
records.insert(patch.purl.clone(), build_patch_record(&patch, files));
}
DetachedDownload {
authoritative,
code: i32::from(batch.failed > 0),
events: batch.events,
warnings: release_warnings(batch.warnings),
Expand Down Expand Up @@ -1399,6 +1470,7 @@ pub(crate) async fn run_nested_apply(
common: GlobalArgs,
json: bool,
report_user_patch_skips: bool,
shown_opt_ins: Vec<crate::ui::ShownOptIn>,
recorded: &[(String, String)],
client: &ApiClient,
lock: LockGuard,
Expand All @@ -1413,6 +1485,7 @@ pub(crate) async fn run_nested_apply(
nested: Some(super::apply::NestedApply {
caller_json: json,
report_user_patch_skips,
shown_opt_ins,
}),
};
// The nested apply runs on the host's client: seeded, never rebuilt.
Expand Down Expand Up @@ -1707,6 +1780,29 @@ pub async fn download_and_apply_patches_into(
// matching the per-patch event (`downloaded` / `updated`) and the
// single-uuid flow's summary in `save_and_apply_patch`; their sum gates
// the apply step.
for fetched in &batch.fetched {
env.register_view_opt_in(
&fetched.patch.purl,
&fetched.patch.uuid,
fetched.patch.requires_opt_in,
fetched.patch.opt_in_note.as_deref(),
);
}
for patch in &batch.metadata_refreshes {
env.register_view_opt_in(
&patch.purl,
&patch.uuid,
patch.requires_opt_in,
patch.opt_in_note.as_deref(),
);
}

let mut metadata_changed = false;
for patch in &batch.metadata_refreshes {
if let Some(record) = manifest.patches.get_mut(&patch.purl) {
metadata_changed |= refresh_opt_in_metadata(record, patch);
}
}
let downloaded = batch.fetched.len();
let mut updated = 0usize;
let mut new_blobs: Vec<String> = Vec::new();
Expand Down Expand Up @@ -1742,7 +1838,7 @@ pub async fn download_and_apply_patches_into(
let added = downloaded - updated;
// Write only when a record changed: an all-skipped or all-failed run
// leaves the manifest bytes (and a fresh project's tree) untouched.
if downloaded > 0 {
if downloaded > 0 || metadata_changed {
if let Err(e) = write_manifest(&manifest_path, &manifest).await {
// The blobs this run just wrote have no record pointing at them:
// unwind exactly those (a pre-existing record's blobs stay).
Expand Down Expand Up @@ -1798,6 +1894,16 @@ pub async fn download_and_apply_patches_into(
);
}

if !quiet {
if let Some(notes) = env.opt_in_note_block() {
println!("\n{notes}");
}
}
let shown_opt_ins: Vec<_> = recorded
.iter()
.filter_map(|(_, uuid)| env.shown_opt_ins(uuid))
.collect();

// Auto-apply unless --save-only (the lock decision above).
let mut apply_report: Option<ApplyRunReport> = None;
if let Some(lock) = apply_lock {
Expand All @@ -1813,6 +1919,7 @@ pub async fn download_and_apply_patches_into(
nested_apply_args_from_params(params, run, &manifest_path),
params.json,
!params.user_patch_skips_reported,
shown_opt_ins,
&recorded,
run.api_client,
lock,
Expand Down
Loading
Loading