[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: C09, C39.
Kind: bug (fixed by one refactor). Source: review Part 2.6 / R2 (C09), plus a new finding (C39): the gap is wider than the review's "get search path".
Problem
The stale-token recovery (a 401/403 from the authenticated API → rebuild the client with build_proxy_fallback_client → retry on the public proxy, free patches only) is written by each command rather than by the API client. Three commands have a copy:
Every other API consumer builds the client with get_api_client_with_overrides and has no recovery at all:
Proof by execution (main 045d7ec, each case run twice, same result). There were two local servers: an "auth" server that answers 401 to everything and a "proxy" server. The environment was SOCKET_API_TOKEN=<stale> SOCKET_ORG_SLUG=acme SOCKET_API_URL=<auth> SOCKET_PROXY_URL=<proxy>.
| Command |
Requests seen |
Result |
get <uuid> --json |
auth GET /v0/orgs/acme/patches/view/<uuid>, then proxy GET /patch/view/<uuid> |
warning "falling back to public patch API proxy", proxy answer used |
get CVE-2021-23337 --json |
auth GET …/by-cve/… only |
{"status":"error","error":"Unauthorized: Invalid API token"} |
get GHSA-… --json, get pkg:npm/lodash@4.17.20 --json |
auth only |
same error |
apply --json --download-mode file (a manifest whose afterHash blob is missing) |
auth GET /v0/orgs/acme/patches/blob/<hash> only |
partialFailure, sources_download_failed, exit 1 |
control: the same apply with no token |
proxy GET /patch/blob/<hash> |
the request reaches the proxy |
So a user whose token was revoked can scan (it falls back, and it saves free patches to the manifest), but apply on a cold blob cache, rollback and repair with the same token can't download those same free blobs, and get CVE-… fails outright.
Symptoms
None filed yet. This is a user-visible inconsistency between commands on the same credentials.
Impact: medium. A stale or revoked token in CI (a common case, since tokens rotate) breaks apply/rollback/repair/get <id>/eject even for free patches, while scan succeeds. The fallback warning text and the api_auth_fallback code are also re-spelled in each copy.
Proposed change
Move the recovery into core, so every consumer gets it and the three CLI copies are deleted:
- Give
ApiClient a one-shot downgrade. On the first is_fallback_candidate error from an authenticated call, the client swaps (once, shared across clones/tasks, for example behind an Arc<OnceLock<ApiClient>>) to the proxy client that build_proxy_fallback_client would build, records that it fell back, and retries that call on the proxy. Later calls go straight to the proxy.
- Expose
client.fell_back_to_proxy() for telemetry's fallback_to_proxy and for the paid-tier check (patch.tier == "paid" && use_public_proxy in get).
- The warning is emitted once, through one function that renders the existing text, and
vex maps it to api_auth_fallback.
- Delete the copies in
scan/mod.rs, get.rs and vex_sources.rs, including their fallback_to_proxy / use_public_proxy mutable locals where those exist only for this.
Size and scope
- Files:
api/client.rs (+80), scan/mod.rs, get.rs, vex_sources.rs (−70), and test updates. Estimated ~250 changed production lines.
- Out of scope: the org-slug fallback route (C07, a separate decision), retry/timeout unification (C15), and
hosted-bundle, which is documented as having no fallback (keep it opted out).
Acceptance criteria
Dependencies
[agent] Filed by the scheduled architecture audit routine (CLI and core). Register: C09, C39.
Kind: bug (fixed by one refactor). Source: review Part 2.6 / R2 (C09), plus a new finding (C39): the gap is wider than the review's "get search path".
Problem
The stale-token recovery (a 401/403 from the authenticated API → rebuild the client with
build_proxy_fallback_client→ retry on the public proxy, free patches only) is written by each command rather than by the API client. Three commands have a copy:scan:scan/mod.rs#L2014-L2035get <uuid>only:get.rs#L2625-L2647vex:vex_sources.rs#L944-L970Every other API consumer builds the client with
get_api_client_with_overridesand has no recovery at all:getCVE / GHSA / PURL / package-name search:get.rs#L2770-L2797,get.rs#L2840-L2853applyon-demand blob/diff downloads:apply.rs#L162-L165andfetch_stage.rsrollbackbefore-blob downloads:rollback.rs#L2534-L2540repair:repair.rs#L404-L415vendoreject view fetches:vendor.rs#L1148-L1196.CLI_CONTRACT.mdline 552 promises eject uses "the same client and public-proxy fallback asget", butvendor.rsnever callsis_fallback_candidateorbuild_proxy_fallback_client.Proof by execution (main
045d7ec, each case run twice, same result). There were two local servers: an "auth" server that answers 401 to everything and a "proxy" server. The environment wasSOCKET_API_TOKEN=<stale> SOCKET_ORG_SLUG=acme SOCKET_API_URL=<auth> SOCKET_PROXY_URL=<proxy>.get <uuid> --jsonGET /v0/orgs/acme/patches/view/<uuid>, then proxyGET /patch/view/<uuid>get CVE-2021-23337 --jsonGET …/by-cve/…only{"status":"error","error":"Unauthorized: Invalid API token"}get GHSA-… --json,get pkg:npm/lodash@4.17.20 --jsonapply --json --download-mode file(a manifest whose afterHash blob is missing)GET /v0/orgs/acme/patches/blob/<hash>onlypartialFailure,sources_download_failed, exit 1applywith no tokenGET /patch/blob/<hash>So a user whose token was revoked can
scan(it falls back, and it saves free patches to the manifest), butapplyon a cold blob cache,rollbackandrepairwith the same token can't download those same free blobs, andget CVE-…fails outright.Symptoms
None filed yet. This is a user-visible inconsistency between commands on the same credentials.
Impact: medium. A stale or revoked token in CI (a common case, since tokens rotate) breaks
apply/rollback/repair/get <id>/eject even for free patches, whilescansucceeds. The fallback warning text and theapi_auth_fallbackcode are also re-spelled in each copy.Proposed change
Move the recovery into core, so every consumer gets it and the three CLI copies are deleted:
ApiClienta one-shot downgrade. On the firstis_fallback_candidateerror from an authenticated call, the client swaps (once, shared across clones/tasks, for example behind anArc<OnceLock<ApiClient>>) to the proxy client thatbuild_proxy_fallback_clientwould build, records that it fell back, and retries that call on the proxy. Later calls go straight to the proxy.client.fell_back_to_proxy()for telemetry'sfallback_to_proxyand for the paid-tier check (patch.tier == "paid" && use_public_proxyinget).vexmaps it toapi_auth_fallback.scan/mod.rs,get.rsandvex_sources.rs, including theirfallback_to_proxy/use_public_proxymutable locals where those exist only for this.Size and scope
api/client.rs(+80),70), and test updates. Estimated ~250 changed production lines.scan/mod.rs,get.rs,vex_sources.rs(−hosted-bundle, which is documented as having no fallback (keep it opted out).Acceptance criteria
is_fallback_candidateandbuild_proxy_fallback_clienthave no callers incrates/socket-patch-cli/src(the fallback lives only in core).get CVE-…,get GHSA-…,get pkg:…,apply(missing blob),rollback(missing before-blob),repairandvendoreject each reach the proxy and succeed for a free patch.paid_required(get) and are skipped as before (scan).--silent/--jsongates as today.tests/scan_api_retry_e2e.rsandcrates/socket-patch-core/tests/api_retry_e2e.rs.CLI_CONTRACT.mdstates the fallback once for every API-using command (the eject sentence becomes true).Dependencies
api/client.rs, as do open PRs Stream patch blob and diff downloads to disk (#571) #607 and Retry patch API connections reset mid-handshake #610: land after them or rebase.RunCtx): the client becomes self-contained, so commands no longer need a mutableapi_client.