Skip to content

feat(cli): agent-paste upgrade binary self-update (AP-165 Phase 3)#252

Merged
isuttell merged 1 commit into
mainfrom
feat/cli-upgrade
Jun 5, 2026
Merged

feat(cli): agent-paste upgrade binary self-update (AP-165 Phase 3)#252
isuttell merged 1 commit into
mainfrom
feat/cli-upgrade

Conversation

@isuttell

@isuttell isuttell commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 3 of the CLI auto-update feature (ADR 0080 §5). Adds agent-paste upgrade: a standalone binary install downloads the matching release asset from the GitHub Release, verifies it against SHA256SUMS, and atomically replaces the running binary in place. This is the command the Phase 2 staleness nag already tells binary users to run, so it closes the last build gap — every channel's hint now points at a working command. Binary self-update is always explicit (the user runs upgrade), never silent.

Phases 1 (version baking, #232) and 2 (update check + KV endpoint + release auto-publish, #244) are already on main.

Changes

  • apps/cli/src/upgrade.ts (new): the command.
    • Tag validation is a security boundary. The release tag is checked against a strict cli-v<semver> pattern before it is interpolated into the download URL. An unvalidated ../-style tag would be path-normalized into a different GitHub repo, and since the asset and its SHA256SUMS would both come from that attacker-controlled base, the checksum check would pass against the attacker's own sums — silent RCE. The validator stops it before any fetch.
    • Verify SHA-256 of the downloaded bytes before any write to the target; refuse on mismatch or a missing checksum, writing nothing. HTTPS-only, fail on any non-2xx.
    • Atomic replace: temp file in the binary's own directory (no EXDEV), then a rename-aside dance (current → .old, new → target, drop .old) on all platforms so a running exe is replaceable on Windows and ETXTBSY-strict Linux. The original is restored if the final rename fails.
    • Permission wall (a sudo'd install dir the user can't write): the verified bytes are re-staged in the writable config dir and the CLI prints an accurate sudo mv <staged> <target> to finish. No silent privilege escalation; the hint always points at a file that exists.
    • OS/arch → asset table ported from apps/apex/src/install-{sh,ps1}.ts, locked by a parity test so the three sources can't drift.
  • apps/cli/src/index.ts: routes upgrade [<tag>] before auth resolution (it needs no client). The pinned version is a positional tag (agent-paste upgrade cli-v1.2.3) to avoid colliding with the --version/-v print flag; the bare version-print shortcut is now gated on there being no subcommand.
  • apps/cli/src/node-globals.d.ts: added process.arch and fs.rename to the CLI's minimal node-types shim.
  • apps/cli/README.md: documents version, upgrade, AGENT_PASTE_NO_UPDATE_CHECK, and the per-channel update-check behavior.
  • docs/ops/cli-auto-update-plan.md: Phase 3 marked done.

Risk: MEDIUM

  • Areas touched: CLI only. Downloads and executes a binary that replaces the running executable (RCE-adjacent), so reviewed accordingly.
  • Security: Two P0s found in local review and fixed — tag-traversal verification bypass, and a broken sudo-recovery hint that pointed at a never-written file. A second adversarial review verified both fixes (including an empirical regex/URL-traversal probe) and found no new issues.
  • Performance: n/a (a manual, on-demand command).
  • Breaking: none. New command; no existing behavior changes.

Test plan

  • pnpm --filter @zaks-io/agent-paste test — 113 pass (26 new upgrade tests + index routing).
  • pnpm verify — 88/88 Turbo tasks + scripts green.
  • pnpm test:coverage — branches 80.37% / lines 89.14% (above the 80% gate); upgrade.ts at 92% lines / 81.81% branches.
  • pnpm lint + pnpm typecheck clean.
  • Real end-to-end self-replace is only meaningful on a compiled binary — verified manually after the next cli-v* release, not in CI (all fetch/fs injected in unit tests).

Issue: AP-165

🤖 Generated with Claude Code

Adds the `agent-paste upgrade` command (ADR 0080 §5): a standalone binary
install downloads the matching release asset from the GitHub Release, verifies
it against SHA256SUMS, and atomically replaces the running binary in place.
Off the binary channel it redirects to the npm/npx updater and exits 1 without
touching the filesystem.

- Validate the release tag against a strict `cli-v<semver>` pattern before it
  reaches the URL: an unvalidated `../` tag would re-point the base at an
  attacker repo whose own SHA256SUMS would "pass" verification.
- Verify SHA-256 before any write to the target; refuse on mismatch or a missing
  checksum, writing nothing.
- Atomic replace: temp in the binary's own dir (no EXDEV), rename-aside dance on
  all platforms (Windows + ETXTBSY-strict Linux), restore the original if the
  final rename fails.
- Permission wall (sudo'd install dir): re-stage the verified bytes in the
  config dir and print an accurate `sudo mv` to finish; no silent escalation.
- OS/arch asset table ported from the install scripts, locked by a parity test.

README documents `version`, `upgrade`, and `AGENT_PASTE_NO_UPDATE_CHECK`.

Issue: AP-165

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jun 5, 2026

Copy link
Copy Markdown
AP-165 CLI auto-update: version baking, update check, and binary self-upgrade

Outcome

The @zaks-io/agent-paste CLI can tell a user — human or agent — that a newer version exists, and the standalone binary channel (which has no package manager behind it) can self-upgrade on explicit request. Implements ADR 0080; phased plan in docs/ops/cli-auto-update-plan.md.

Context

Three distribution channels with different update mechanics: standalone binary (bun --compile, GitHub Releases, ~/.local/bin), npm i -g, and npx. Today the running CLI does not even know its own version (neither build path injects it), so it cannot detect staleness. The version source of truth will be a new unauthenticated GET /v1/public/cli-version route backed by a CLI_RELEASE KV value, so advertising a new version is a data write, not an api redeploy.

Scope (phased)

  • Phase 1 (this PR): bake package.json version into the bundle (esbuild define) and the binary (bun build --define); add version / --version / -v.
  • Phase 2: unauthenticated cli.version route + KV-backed {latest, min_supported}; background, throttled (24h), silenceable update check that prints a per-channel nag to stderr (npx: none; npm-global: npm i -g ...@latest; binary: agent-paste upgrade). Edge-cached (Cache-Control) + module-scope memo, not the two-layer auth cache.
  • Phase 3: agent-paste upgrade — verified download (SHA256SUMS) + atomic self-replace, reusing the install.sh flow.
  • Phase 4: release pipeline writes the KV value on publish (GitHub release: publishedwrangler kv key put); reconcile npm version ↔ cli-vX.Y.Z tag.

Out of scope

Silent binary self-update (explicit upgrade only); auto-publishing to npm; runtime package.json read for the version.

Acceptance criteria

  • Phase 1: agent-paste version / --version / -v print the package version; compiled binary prints the same; dev/test runs print 0.0.0-dev without throwing.
  • Phase 2–4 per the plan doc.

Required checks

pnpm verify + pnpm test:coverage (CI Validate).

Review in Linear

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0b9ec8e6-5543-44e7-a31a-8287896c87a3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-upgrade

Comment @coderabbitai help to get the list of available commands and usage tips.

@isuttell isuttell merged commit be60c1f into main Jun 5, 2026
10 checks passed
@isuttell isuttell deleted the feat/cli-upgrade branch June 5, 2026 00:56
@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

agent-paste PR preview resources were cleaned up. The shared Preview GitHub Environment is retained for future preview deploys.

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