Skip to content

feat: socket-patch --update self-update + passive update notifier#128

Open
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
feat/self-update
Open

feat: socket-patch --update self-update + passive update notifier#128
Mikola Lysenko (mikolalysenko) wants to merge 4 commits into
mainfrom
feat/self-update

Conversation

@mikolalysenko

Copy link
Copy Markdown
Collaborator

What

`socket-patch --update` — the CLI updates itself from GitHub Releases, closing the loop with the curl|sh install flow:

  • Resolves the latest version via the `releases/latest` redirect (API-JSON fallback), or installs an explicit pin (`--update 3.4.0` / `SOCKET_PATCH_VERSION` — the same pin env install.sh and the launchers honor).
  • Downloads `socket-patch-.tar.gz|.zip` (target embedded at build time via `build.rs` — no runtime libc guessing), verifies against the published `SHA256SUMS` before extraction, sanity-execs the staged binary, then swaps with one atomic rename (mode-preserving; setuid/setgid and Linux setcap targets refused; Windows via the `self-replace` rename-dance).
  • Detects package-manager-managed installs from the canonicalized exe path (npm / pip / cargo / gem+composer launcher cache / Homebrew) and refuses with that manager's own upgrade command; `--force` overrides.
  • `--dry-run` = check-only probe (zero downloads, `updateAvailable` in `--json` details); `--offline` refuses up front and `--force` can't bypass it; per-environment single-flight via flock.
  • Surface is the root `--update` flag, rewritten (same mechanism as the bare-UUID→`get` shortcut) onto a hidden internal subcommand.

Passive update notice — interactive runs mention a newer release at most once/day, stderr-only, after command output; suppressed under `--json`/`--silent`/offline/CI/non-TTY/`SOCKET_NO_UPDATE_CHECK=1` (suppressed ⇒ zero network I/O); can never change an exit code, touch stdout, or add >~500 ms; state corruption degrades to "never checked".

Also: `shellcheck scripts/install.sh` in the lint job (and a fix for the SC2144 musl-probe bug it found).

Reliability & testing

All hermetic (wiremock fake-release fixture; hostile-seeded env scrub keeps every pre-existing suite off the network — belt-and-braces kill-switch in `.cargo/config.toml` `[env]` + both test harnesses):

  • Crux e2e: a staged copy of the real binary genuinely replaces itself — byte-diff proof on Linux/Windows (trailered payload), inode-change rename evidence on macOS; `CARGO_BIN_EXE` hash re-verified untouched after every test.
  • Failure matrix (25 tests): checksum mismatch, missing SUMS entry/file (refuses before wasting the download), asset 404, bounded timeout, truncation, downgrade rules, garbage tags, sanity-exec failure ordering, EACCES hint, offline-beats-force with zero requests, lock contention, envelope shapes — every row ends binary-intact + no droppings.
  • Channel detection (18): real crafted install shapes (can't fake `current_exe` — so we don't), refusal-precedes-network, symlink canonicalization, `--force` override.
  • Notifier (30): cadence via state-file timestamps (no clock mocking), cached-notice zero-network, full guard matrix proving guards suppress the fetch, grace-budget wall-clock bound, corrupt/future/unwritable state tolerance, one PTY test for the true-TTY path.
  • Core units (~100): SUMS parser grammar, semver precedence (build-metadata pitfall), Location parsing, channel table incl. Windows spellings, state skew rules, caps/timeouts, swap mode/setuid/lock semantics, the 14-triple release-matrix pin.
  • Adversarially reviewed (multi-agent find→verify pass); all four confirmed findings fixed in the last commit.

Live smoke against real GitHub is `#[ignore]`-gated: `cargo test -p socket-patch-cli --test self_update_e2e -- --ignored` (documented in ci.yml next to the other live-API suites; most useful right after a release).

Notes for review

  • Checksum-only trust (HTTPS + GitHub origin) — same posture as install.sh/launchers; sigstore/minisign is a named follow-up.
  • CLI_CONTRACT.md gains the full self-update section (flags, error codes, notifier promises, internal env knobs). Additive surface → MINOR-safe (or rides the planned v4.0.0).
  • The four `setup_matrix_*` local failures and one docker composer flake seen during the full sweep are pre-existing/environmental (transient network in-container, spawn starvation under load) — verified by rerunning each green.

🤖 Generated with Claude Code

…re engine, CLI wiring, crux e2e)

- core update engine: release resolution (redirect probe + API fallback),
  SHA256SUMS verify-before-extract, single-entry archive extraction with
  caps, dest-dir staging + sanity exec, mode-preserving atomic swap
  (self-replace on Windows), flock single-flight, channel detection
- CLI: root --update flag via argv rewrite to hidden self-update
  subcommand; SOCKET_PATCH_VERSION pin; --force; envelope command=update
- passive notifier: guarded once-daily background check, 500ms grace,
  stderr-only notice, SOCKET_NO_UPDATE_CHECK opt-out
- hermeticity: notifier kill-switch in .cargo [env] + test harnesses
- shellcheck install.sh in CI (and fix its SC2144 musl-loader probe)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… suites

25 + 18 + 30 tests, all hermetic (wiremock fake release / closed ports):
- failures: checksum/sums/404/timeout/truncation/downgrade/pin/sanity/
  EACCES/offline-beats-force/lock-held/json-envelope rows, each ending
  binary-intact + no-droppings
- channels: npm/pip/cargo/launcher-cache refusals with per-manager hints,
  zero-network-before-refusal, --force override, symlink canonicalization
- notifier: cadence via state-file timestamps, cache-notice zero-network,
  guard matrix (fetch suppressed, not just print), grace budget bound,
  corrupt/future/unwritable state tolerance, PTY true-TTY proof

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- HTTPS-only redirect hops now enforced for SHA256SUMS and API-fallback
  fetches too, not just the archive download (the SUMS leg is the
  integrity root; shared follow_redirect_policy)
- --update --dry-run on an up-to-date install now reports the documented
  verified/update_check probe shape (updateAvailable: false) instead of
  falling into the skipped/already_latest path
- stage sweep is age-gated (1h) so a concurrent updater with a divergent
  state dir can no longer have its live stage deleted mid-pipeline;
  contract wording now states the lock's per-environment scope honestly
- Linux file capabilities (setcap xattr) on the target now refuse the
  swap, same rationale as setuid: an unprivileged rename cannot restore
  them
- fixture: corrupt_sums_entry_for matches the full '  <name>' suffix so
  it cannot corrupt a different asset's line
- e2e: cli_global_args matrix covers self-update; new up-to-date dry-run
  probe regression test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​self-replace@​1.5.010010094100100

View full report

@socket-security-staging

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedcargo/​self-replace@​1.5.010010094100100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant