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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Every subcommand accepts the same set of "global" flags via a single shared `Glo
| `--manifest-path` | — | `SOCKET_MANIFEST_PATH` | `.socket/manifest.json` | path | Manifest location (resolved relative to `--cwd`) |
| `--api-url` | — | `SOCKET_API_URL` | `https://api.socket.dev` | string | Authenticated API endpoint |
| `--api-token` | — | `SOCKET_API_TOKEN` | (none) | string | Auth token (absence selects the public proxy) |
| `--org` | `-o` | `SOCKET_ORG_SLUG` | (auto-resolve) | string | Org slug |
| `--org` | `-o` | `SOCKET_ORG_SLUG` | (auto-resolve) | string | Org slug. Resolved once per run from the token (`GET /v0/organizations`) when omitted; if that fails, the whole run uses the public proxy anonymously (free patches only) and warns. |
| `--proxy-url` | — | `SOCKET_PROXY_URL` | `https://patches-api.socket.dev` | string | Public proxy when no token |
| `--ecosystems` | `-e` | `SOCKET_ECOSYSTEMS` | (all) | CSV → `Vec<String>` | Restrict to these ecosystems |
| `--download-mode` | — | `SOCKET_DOWNLOAD_MODE` | **`diff`** | enum: `diff` \| `file` (`package` was removed and is rejected) | Patch artifact format |
Expand Down Expand Up @@ -331,7 +331,7 @@ Contract details:
* **JSON success surface**: `apply` adds a top-level `vex` object to its envelope; `scan` adds a top-level `vex` key to its result. Both carry `{ path, statements, format: "openvex-0.2.0" }`.
* `apply`'s no-manifest early exit (the `noManifest` success no-op; v5.0: its human line is `No patch manifest found; nothing to apply.` — it names the missing `.socket/manifest.json`, not the folder, since `.socket/` may legitimately hold vendored state) and `vendor`'s (`No manifest found, nothing to vendor.` — a project with hosted pins ejects instead, v5.0) still generate the document from the lockfiles and the vendor ledger (manifest-less VEX: hosted / vendored checkouts carry no manifest). Nothing referenced anywhere keeps the calm exit 0 (a stale document at the path is removed; `--json` carries any discovery diagnostics in `warnings[]`); any other VEX failure fails the command with exit 1 — including a run whose only candidates are omitted `record_unavailable` (an `--offline` run over a lockfile-wired checkout with no local records), so an ambient `SOCKET_VEX` there fails the install. `--dry-run` skips generation on both, and so does `apply --check` — it stays read-only and offline-safe, leaving the output path untouched. `scan` has no such early exit: with no manifest and nothing wired anywhere its `--vex` fails with `manifest_not_found`.
* **Stale-doc removal (v3.5)**: a run that ends in a VEX error removes a recognizably-OpenVEX file (JSON whose `@context` names openvex.dev) already sitting at the output path — a pipeline reusing one path can never ship yesterday's attestation for a now-unpatched tree. Unrelated files at the path are never touched; a mid-write partial that no longer parses as JSON is left for downstream parsers to reject loudly.
* **Additive warnings (v3.5)**: `product_not_iri` (the `--product`/`--vex-product` override is neither a `pkg:` purl nor an absolute IRI; honored verbatim, warned) and `vendored_tree_out_of_sync` (a healthy vendored attestation stands on the committed artifact + lock wiring while the PRESENT installed tree hash-mismatches the patched bytes — run the package manager's install (for a project with Hatch environments the detail also names `hatch env remove <env>`, since Hatch keeps an installed release); the attestation itself is unchanged). Both ride stderr in human mode and `warnings[]` in the standalone `vex --json` envelope. Same channel for `product_multiple_manifests` (auto-detect found several project manifests and names the one it used), `vex_stale_doc_removed` (the stale-doc removal above happened), the manifest-less plan's advisories — `vex_wiring_conflict` (the lockfiles wire a package to different patches: which files, which uuids, how to fix it), `vex_record_superseded` (a recorded patch replaced by the lockfile-wired one), `vex_claim_unwired` (a ledger claim whose patch the lockfiles still mention, but not as wiring), `vex_record_offline` / `vex_record_not_found` / `vex_record_fetch_failed` (why a lockfile-wired patch has no record — the detail behind a `record_unavailable` skip) and `api_auth_fallback` (the authenticated API refused the credentials and the public proxy served free patches only; `get` / `scan`'s warning text) — and, standalone only, `org_looks_like_path` (`-o`/`--org` given a file-shaped value — `-O` is `--output`). The standalone error envelope carries `warnings[]` too. An embedded `--vex` that fails also folds each omitted patch into the host command's `warnings[]` as `vex_omitted` (`<purl>: <why> (<errorCode>)` — standalone `vex` lists them as `skipped` events), and `--silent` lists them as `omitted: <purl> (<errorCode>)` lines under the error. A corrupt `.socket/vendor/state.json` is no longer degraded with a warning: every form of vex fails with `vendor_ledger_corrupt` (see the error-code table). A malformed pre-v5 `redirect-state.json` is, as of v5.0, only the `redirect_ledger_corrupt` warning (hosted mode keeps no ledger; the file is an optional migration record source).
* **Additive warnings (v3.5)**: `product_not_iri` (the `--product`/`--vex-product` override is neither a `pkg:` purl nor an absolute IRI; honored verbatim, warned) and `vendored_tree_out_of_sync` (a healthy vendored attestation stands on the committed artifact + lock wiring while the PRESENT installed tree hash-mismatches the patched bytes — run the package manager's install (for a project with Hatch environments the detail also names `hatch env remove <env>`, since Hatch keeps an installed release); the attestation itself is unchanged). Both ride stderr in human mode and `warnings[]` in the standalone `vex --json` envelope. Same channel for `product_multiple_manifests` (auto-detect found several project manifests and names the one it used), `vex_stale_doc_removed` (the stale-doc removal above happened), the manifest-less plan's advisories — `vex_wiring_conflict` (the lockfiles wire a package to different patches: which files, which uuids, how to fix it), `vex_record_superseded` (a recorded patch replaced by the lockfile-wired one), `vex_claim_unwired` (a ledger claim whose patch the lockfiles still mention, but not as wiring), `vex_record_offline` / `vex_record_not_found` / `vex_record_fetch_failed` (why a lockfile-wired patch has no record — the detail behind a `record_unavailable` skip) and `api_auth_fallback` (the public proxy served free patches only: the authenticated API refused the credentials, `get` / `scan`'s warning text; or, v5.0, a token was set with no `--org` / `SOCKET_ORG_SLUG` / socket-cli `defaultOrg` and the org could not be resolved, so the whole run used the proxy — standalone `vex --json` carries it when it fetched records, and `scan --json` / `get --json` report this case in their top-level `warnings[]`) — and, standalone only, `org_looks_like_path` (`-o`/`--org` given a file-shaped value — `-O` is `--output`). The standalone error envelope carries `warnings[]` too. An embedded `--vex` that fails also folds each omitted patch into the host command's `warnings[]` as `vex_omitted` (`<purl>: <why> (<errorCode>)` — standalone `vex` lists them as `skipped` events), and `--silent` lists them as `omitted: <purl> (<errorCode>)` lines under the error. A corrupt `.socket/vendor/state.json` is no longer degraded with a warning: every form of vex fails with `vendor_ledger_corrupt` (see the error-code table). A malformed pre-v5 `redirect-state.json` is, as of v5.0, only the `redirect_ledger_corrupt` warning (hosted mode keeps no ledger; the file is an optional migration record source).

### VEX provenance markers (contract)

Expand Down Expand Up @@ -1045,7 +1045,7 @@ Empty string means unset at every layer: exported-but-empty flag-bound vars are
| `SOCKET_MANIFEST_PATH` | `--manifest-path` | `.socket/manifest.json` | — |
| `SOCKET_API_URL` | `--api-url` | `https://api.socket.dev` | — |
| `SOCKET_API_TOKEN` | `--api-token` | (none) | Absence selects the public proxy. |
| `SOCKET_ORG_SLUG` | `--org` / `-o` | (auto-resolve) | — |
| `SOCKET_ORG_SLUG` | `--org` / `-o` | (auto-resolve) | Resolved once per run from the token (`GET /v0/organizations`) when omitted; if that fails, the whole run uses the public proxy anonymously (free patches only) and warns. |
| `SOCKET_PROXY_URL` | `--proxy-url` | `https://patches-api.socket.dev` | — |
| `SOCKET_ECOSYSTEMS` | `--ecosystems` / `-e` | (all) | Comma-separated list. |
| `SOCKET_DOWNLOAD_MODE` | `--download-mode` | `diff` | One of `diff` / `file`. |
Expand Down
10 changes: 10 additions & 0 deletions crates/socket-patch-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use socket_patch_core::api::client::{
};
use socket_patch_core::constants::DEFAULT_PATCH_MANIFEST_PATH;
use socket_patch_core::crawlers::Ecosystem;
use socket_patch_core::telemetry::TelemetryAuth;
use socket_patch_core::vendor::{VendorServiceConfig, VendorSource};

/// clap value-parser for each `--ecosystems` / `SOCKET_ECOSYSTEMS` token.
Expand Down Expand Up @@ -569,6 +570,15 @@ impl GlobalArgs {
resolve_ambient_credentials(overrides.api_token, overrides.org_slug)
}

/// Telemetry's route for a run that built no API client:
/// [`Self::telemetry_credentials`] as a [`TelemetryAuth`] (the org
/// endpoint only for a token + slug; never a network call). A run that
/// has a client uses [`TelemetryAuth::for_client`] instead.
pub(crate) fn telemetry_auth(&self) -> TelemetryAuth {
let (api_token, org_slug) = self.telemetry_credentials();
TelemetryAuth::from_credentials(api_token.as_deref(), org_slug.as_deref())
}

/// The vendoring-service config every vendor entry point (`vendor`,
/// `scan`/`get --mode vendored`) builds from the same flags —
/// `--vendor-source` / `--vendor-url` / `--patch-server-url` /
Expand Down
48 changes: 18 additions & 30 deletions crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use socket_patch_core::patch::redirect::golang_local::{
apply_go_redirect, reconcile_go_redirects, verify_go_redirect_state,
};
use socket_patch_core::patch::sidecars::{maven as maven_sidecars, SidecarAdvisoryCode};
use socket_patch_core::telemetry::{track_patch_applied, track_patch_apply_failed};
use socket_patch_core::telemetry::{track_patch_applied, track_patch_apply_failed, TelemetryAuth};
use socket_patch_core::utils::purl::parse_golang_purl;
use socket_patch_core::utils::purl::{normalize_purl, strip_purl_qualifiers};
use socket_patch_core::utils::purl_key::PurlKey;
Expand Down Expand Up @@ -1099,7 +1099,7 @@ pub async fn run(args: ApplyArgs) -> i32 {
println!("No patch manifest found; nothing to apply.");
}
let vex_result = if !args.common.dry_run && !args.check && args.vex.vex.is_some() {
let params = args.vex.to_build_params();
let params = args.vex.to_build_params(None);
Some(generate_vex_without_manifest(&args.common, &params, &manifest_path).await)
} else {
None
Expand Down Expand Up @@ -1286,8 +1286,7 @@ pub(crate) async fn run_locked(
client: &ApiClient,
lock: LockGuard,
) -> ApplyRunReport {
let api_token = client.api_token().cloned();
let org_slug = client.org_slug().cloned();
let telemetry = TelemetryAuth::for_client(client);

// ONE parse of the manifest for the whole run — the PnP gate and the
// apply loop (embedded VEX re-reads it by design, after the writes).
Expand All @@ -1298,13 +1297,13 @@ pub(crate) async fn run_locked(
Ok(Some(m)) => m,
Ok(None) => {
lock.release();
let code = report_apply_failure(&args, "Invalid manifest", &api_token, &org_slug).await;
let code = report_apply_failure(&args, "Invalid manifest", &telemetry).await;
return ApplyRunReport::run_failure(code, "apply_failed", "Invalid manifest");
}
Err(e) => {
lock.release();
let error = e.to_string();
let code = report_apply_failure(&args, &error, &api_token, &org_slug).await;
let code = report_apply_failure(&args, &error, &telemetry).await;
return ApplyRunReport::run_failure(code, "apply_failed", error);
}
};
Expand Down Expand Up @@ -1464,7 +1463,7 @@ pub(crate) async fn run_locked(
// `no_applicable_patches`, and would write an attestation
// file during --dry-run. Skip instead.
let vex_result = if success && !args.common.dry_run && args.vex.vex.is_some() {
let params = args.vex.to_build_params();
Comment thread
mikolalysenko marked this conversation as resolved.
let params = args.vex.to_build_params(Some(client));
Some(generate_vex_from_manifest_path(&args.common, &params, &manifest_path).await)
} else {
None
Expand Down Expand Up @@ -1541,6 +1540,13 @@ pub(crate) async fn run_locked(
// `warnings[]` is their machine channel — stderr is
// suppressed under --json.
env.warnings.extend(run_warnings.iter().cloned());
// A token whose org could not be resolved put the run on the
// proxy (stderr already said so); the embedded `--vex` reuses
// this client and leaves reporting it to the host.
env.warnings
.extend(crate::commands::vex_sources::api_auth_fallback_warning(
client,
));
if !success {
env.mark_partial_failure();
}
Expand Down Expand Up @@ -1610,19 +1616,12 @@ pub(crate) async fn run_locked(

// Track telemetry
if success {
track_patch_applied(
patched_count,
args.common.dry_run,
api_token.as_deref(),
org_slug.as_deref(),
)
.await;
track_patch_applied(patched_count, args.common.dry_run, &telemetry).await;
} else {
track_patch_apply_failed(
"One or more patches failed to apply",
args.common.dry_run,
api_token.as_deref(),
org_slug.as_deref(),
&telemetry,
)
.await;
}
Expand Down Expand Up @@ -1672,7 +1671,7 @@ pub(crate) async fn run_locked(
}
Err(e) => {
lock.release();
let code = report_apply_failure(&args, &e, &api_token, &org_slug).await;
let code = report_apply_failure(&args, &e, &telemetry).await;
ApplyRunReport::run_failure(code, "apply_failed", e)
}
}
Expand All @@ -1683,19 +1682,8 @@ pub(crate) async fn run_locked(
/// `--silent` ("errors only", never "nothing" — exit 1 with no message
/// would be undiagnosable), exit 1. Shared by the manifest read in `run`
/// and `apply_patches_inner`'s `Err` arm.
async fn report_apply_failure(
args: &ApplyArgs,
error: &str,
api_token: &Option<String>,
org_slug: &Option<String>,
) -> i32 {
track_patch_apply_failed(
error,
args.common.dry_run,
api_token.as_deref(),
org_slug.as_deref(),
)
.await;
async fn report_apply_failure(args: &ApplyArgs, error: &str, telemetry: &TelemetryAuth) -> i32 {
track_patch_apply_failed(error, args.common.dry_run, telemetry).await;
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.dry_run = args.common.dry_run;
Expand Down
3 changes: 1 addition & 2 deletions crates/socket-patch-cli/src/commands/fetch_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,8 +486,7 @@ mod tests {
ApiClient::new(socket_patch_core::api::client::ApiClientOptions {
api_url: "http://127.0.0.1:1".to_string(),
api_token: None,
use_public_proxy: false,
org_slug: None,
route: socket_patch_core::api::client::ApiRoute::Proxy,
})
}

Expand Down
Loading
Loading