Dashboard › cli › Session 0KWcBFsp6vvB
0KWcBFsp6vvB7enW9Perform a strictly read-only security and abuse-case review of getsentry/cli PR #1569 at exact base bd065083c and exact head 4094ae9a7 in worktree /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade. Inspect every changed file and relevant surrounding implementation/tests. Focus on supply-chain source confusion, downgrade/fallback manipulation, HTTP status classification, credential/header leakage across GitHub/GHCR/Azure redirects, shell injection/word splitting/temp-file/symlink races, untrusted API/manifests/tags, cleanup, cancellation, malformed payloads, and telemetry safety. Contract: exact Toolkit then legacy order, fallback only genuine 404, selected source never mixes, installer definition parity, telemetry never blocks/fails install. Review only; never modify files or VCS. Report substantive findings first with PASS/CONCERN/MUST-FIX, exact file:line evidence, deterministic regression for each defect, and concrete fix. If no defect exists, state PASS and residual risks. End exactly MERGE or DO-NOT-MERGE.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade</path> <type>directory</type> <entries> .craft.yml .git .github/ .gitignore .lore.md .nojekyll .npmrc AGENTS.md apps/ CHANGELOG.md codemods/ docs/ LICENSE.md node_modules/ package.json packages/ pnpm-lock.yaml pnpm-workspace.yaml README.md (19 entries) </entries>
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/AGENTS.md</path> <type>file</type> <content> 1: # Jared (Outpost agent) 2: 3: Autonomous GitHub coding agent. Work in `/workspace/repo`. 4: 5: ## Model tiers 6: 7: The primary model is chosen per event (see `src/agents/models.ts`): heavy for 8: code-producing situations, cheaper for lightweight ones. 9: 10: | Role | Subagent | Model | 11: | --- | --- | --- | 12: | Triage / plan / review (heavy) | (primary Jared) | Claude Opus 4.8 | 13: | Triage / plan / review (light) | (primary Jared) | xAI Grok 4.3 | 14: | Explore | `explore` | OpenAI gpt-5-mini | 15: | Implement | `implement` | Moonshot kimi-k2.7-code | 16: | Ship (commit/push/PR) | `ship` | xAI Grok (`grok-build-0.1`) | 17: 18: Pipeline: triage → explore → plan → implement → review → ship. 19: (`worker` is a deprecated alias of `implement`.) 20: 21: Operators also talk to Jared directly from the Outpost dashboard. ...
bd065083c3b82677747b97a6cc7f5cf57532d3bc 4094ae9a7670f2bf46898fc7d5b8c7d89cf70819 4094ae9a7670f2bf46898fc7d5b8c7d89cf70819 M .github/workflows/ci.yml M package.json M packages/cli/install M packages/cli/package.json M packages/cli/src/commands/cli/upgrade.ts M packages/cli/src/lib/binary.ts M packages/cli/src/lib/delta-upgrade.ts M packages/cli/src/lib/ghcr.ts M packages/cli/src/lib/release-notes.ts M packages/cli/src/lib/upgrade.ts M packages/cli/src/lib/version-check.ts M packages/cli/test/commands/cli/upgrade.test.ts M packages/cli/test/lib/binary.test.ts M packages/cli/test/lib/delta-upgrade.test.ts M packages/cli/test/lib/ghcr.test.ts M packages/cli/test/lib/install-script.test.ts M packages/cli/test/lib/release-notes.test.ts M packages/cli/test/lib/upgrade.test.ts M packages/cli/test/lib/version-check.test.ts .github/workflows/ci.yml | 1 + package.json | 3 +- packages/cli/install | 192 ...
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 67ec71067..6f018db43 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -168,160 +168,161 @@ jobs:
ref: ${{ steps.token.outcome == 'success' && (github.head_ref || github.ref_name) || '' }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION_22 }}
- uses: actions/cache@v5
id: cache
with:
path: |
node_modules
packages/*/node_modules
apps/*/node_modules
key: node-modules-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'packages/cli/patches/**') }}
- if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
- name: Generate API Schema
run: pnpm run generate:schema
- name: Generate docs and skill files
run: pnpm run generate:docs
- name: Validate ...
diff --git a/packages/cli/src/lib/binary.ts b/packages/cli/src/lib/binary.ts
index 75582612a..e30633c7e 100644
--- a/packages/cli/src/lib/binary.ts
+++ b/packages/cli/src/lib/binary.ts
@@ -5,327 +5,461 @@
* Used by both `setup --install` (fresh installs) and `upgrade` (self-updates).
*/
import { spawnSync } from "node:child_process";
import {
existsSync,
mkdirSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { chmod, copyFile, mkdir, realpath, unlink } from "node:fs/promises";
import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
import { compare as semverCompare } from "semver";
import { getUserAgent } from "./constants.js";
import {
buildTlsErrorDetail,
customFetch,
isTlsCertError,
} from "./custom-ca.js";
import { stringifyUnknown, UpgradeError } from "./errors.js";
import { logger } from "./logger.js";
import { isProcessRunning } from "./process-utils.js";
/** Known ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/binary.ts</path>
<type>file</type>
<content>
285: /** Configuration for selecting the first available upgrade source. */
286: export type ResolveUpgradeSourceOptions = {
287: /** Build the source-specific URL whose response proves source availability. */
288: readonly getProbeUrl: (source: UpgradeSource) => string;
289: /** Fetch implementation used for the probe. Defaults to the CLI CA-aware fetch. */
290: readonly fetch?: typeof fetch;
291: /** Optional cancellation signal shared by every source probe. */
292: readonly signal?: AbortSignal;
293: /** Ordered sources to probe. Defaults to all configured upgrade sources. */
294: readonly sources?: readonly UpgradeSource[];
295: };
296:
297: async function fetchUpgradeProbe(
298: source: UpgradeSource,
299: options: ResolveUpgradeSourceOptions
300: ): Promise<Response> {
301: try ...
diff --git a/packages/cli/src/lib/upgrade.ts b/packages/cli/src/lib/upgrade.ts
index 945adc48d..0b9637d8c 100644
--- a/packages/cli/src/lib/upgrade.ts
+++ b/packages/cli/src/lib/upgrade.ts
@@ -1,209 +1,243 @@
/**
* Upgrade Module
*
* Detects how the CLI was installed and provides self-upgrade functionality.
* Binary management helpers (download URLs, locking, replacement) live in
* binary.ts and are shared with the setup --install flow.
*/
import { spawn } from "node:child_process";
import {
chmodSync,
closeSync,
existsSync,
openSync,
realpathSync,
statSync,
unlinkSync,
writeSync,
} from "node:fs";
import { writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, isAbsolute, join, sep } from "node:path";
import { setTimeout } from "node:timers/promises";
import {
acquireLock,
cleanupOldBinary,
determineInstallDir,
fetchWithUpgradeError,
- GITHUB_RELEASES_URL,
getBinaryDownloadUrl,
...
Found 2 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/upgrade.ts:
Line 700: export async function versionExists(
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/commands/cli/upgrade.ts:
Line 297: if (!(await versionExists(lookupMethod, target))) {
diff --git a/packages/cli/src/commands/cli/upgrade.ts b/packages/cli/src/commands/cli/upgrade.ts index 8d8282368..610a12025 100644 --- a/packages/cli/src/commands/cli/upgrade.ts +++ b/packages/cli/src/commands/cli/upgrade.ts @@ -1,1050 +1,1096 @@ /** * sentry cli upgrade * * Self-update the Sentry CLI to the latest or a specific version. * After upgrading, spawns the NEW binary with `cli setup` to update * completions, agent skills, and record installation metadata. * * Supports two release channels: * - stable (default): tracks the latest GitHub release * - nightly: tracks the rolling nightly prerelease built from main * * The channel can be set via --channel or by passing "nightly"/"stable" * as the version argument. The choice is persisted in the local database * so that subsequent bare `sentry cli upgrade` calls use the same channel. ...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_086704793001TliDAg5QgvP0uS
tags,
opts.currentVersion,
opts.targetVersion
);
if (chainTags.length === 0 || chainTags.length > MAX_NIGHTLY_CHAIN_DEPTH) {
return null;
}
let manifests: OciManifest[];
// biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
try {
manifests = await Promise.all(
chainTags.map((tag) => client.fetchManifest(opts.token, tag, opts.signal))
);
} catch {
return null;
}
const binaryName = getPlatformBinaryName();
const patchLayerName = `${binaryName}.patch`;
const digests: string[] = [];
const steps: { fromVersion: string; toVersion: string }[] = [];
let previousVersion = opts.currentVersion;
let totalSize = 0;
let expectedSha256 = "";
for (const [index, manifest] of manifests.entries()) {
const tag = ...
diff --git a/packages/cli/src/lib/delta-upgrade.ts b/packages/cli/src/lib/delta-upgrade.ts
index ec709b8c8..d7f2e3bfa 100644
--- a/packages/cli/src/lib/delta-upgrade.ts
+++ b/packages/cli/src/lib/delta-upgrade.ts
@@ -1,627 +1,648 @@
/** Delta upgrade discovery and application backed by binpatch. */
import { join } from "node:path";
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import {
applyPatchChainInMemory,
extractStableChain as binpatchExtractStableChain,
filterAndSortChainTags as binpatchFilterAndSortChainTags,
validateChainStep as binpatchValidateChainStep,
type DeltaTelemetry,
type DeltaUnavailableReason,
type ExtractStableChainOpts,
type GitHubRelease,
getPatchFromVersion,
getPatchTargetSha256,
ghcrSource,
githubReleaseSource,
type InstrumentHook,
MAX_NIGHTLY_CHAIN_DEPTH,
makeCache,
OciClient,
type OciManifest,
...
No files found
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch</path> <type>directory</type> <entries> dist/ LICENSE package.json README.md (4 entries) </entries>
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist</path> <type>directory</type> <entries> index.d.ts index.js index.js.map (3 entries) </entries>
Found 19 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js:
Line 610: function makeCache(cacheDir) {
Line 654: async function resolveAndApply(opts) {
Line 830: Authorization: `Bearer ${token}`,
Line 855: Authorization: `Bearer ${token}`,
Line 887: * Download a blob by digest. The registry returns a 3xx redirect to blob
Line 888: * storage; the redirect is followed manually without the auth header.
Line 896: Authorization: `Bearer ${token}`,
Line 899: redirect: "manual",
Line 911: const redirectUrl = blobResponse.headers.get("location");
Line 912: if (!redirectUrl) {
Line 915: `Registry blob redirect (${blobResponse.status}) had no Location header`
Line 918: let redirectResponse;
Line 920: redirectResponse = await this.fetch(redirectUrl, {
Line 922: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
580: join(cacheDir, chainFileName(meta.fromVersion, meta.toVersion))
581: ).catch(() => {
582: })
583: );
584: }
585: await Promise.all(deletions);
586: }
587: async function cleanupPatchCache(cacheDir) {
588: let files;
589: try {
590: files = await readdir(cacheDir);
591: } catch (err) {
592: if (isNotFound(err)) return;
593: throw err;
594: }
595: await removeExpiredEntries(cacheDir, files, Date.now());
596: }
597: async function clearPatchCache(cacheDir) {
598: let files;
599: try {
600: files = await readdir(cacheDir);
601: } catch (err) {
602: if (isNotFound(err)) return;
603: throw err;
604: }
605: await Promise.all(
606: files.map((file) => unlink(join(cacheDir, file)).catch(() => {
607: }))
608: );
609: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
430: return [writeFile(filePath, patch.data)];
431: })
432: );
433: if (steps.length > 0) {
434: const firstStep = steps.at(0);
435: const lastStep = steps.at(-1);
436: if (firstStep && lastStep) {
437: const meta = {
438: fromVersion: firstStep.fromVersion,
439: toVersion: lastStep.toVersion,
440: expectedSha256: chain.expectedSha256,
441: cachedAt: Date.now(),
442: patches: steps.map((s, i) => ({
443: fromVersion: s.fromVersion,
444: toVersion: s.toVersion,
445: size: chain.patches[i]?.size ?? 0
446: }))
447: };
448: const metaPath = join(
449: cacheDir,
450: chainFileName(firstStep.fromVersion, lastStep.toVersion)
451: );
452: await ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/node_modules/binpatch/dist/index.js</path>
<type>file</type>
<content>
370: output.set(chunk, offset);
371: offset += chunk.byteLength;
372: onBytes?.(chunk.byteLength);
373: });
374: return output;
375: }
376: function applyPatchToMemory(oldFile, patchData) {
377: return applyReaderToMemory(new MemoryOldReader(oldFile), patchData);
378: }
379: async function applyPatchChainInMemory(oldPath, patches, destPath, onBytes) {
380: if (patches.length === 0) {
381: throw new Error("Cannot apply an empty patch chain");
382: }
383: let reader = await loadOldBinary(oldPath);
384: try {
385: for (let i = 0; i < patches.length - 1; i++) {
386: const patch = patches[i];
387: if (!patch) {
388: throw new Error(`Missing patch at index ${i}`);
389: }
390: const next = await applyReaderToMemory(reader, patch, ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/delta-upgrade.ts</path>
<type>file</type>
<content>
55: StableChainInfo,
56: } from "binpatch";
57: // biome-ignore lint/performance/noBarrelFile: preserve the existing public API
58: export {
59: extractSha256,
60: getPatchFromVersion,
61: getPatchTargetSha256,
62: getStableTargetSha256,
63: PATCH_TAG_PREFIX,
64: } from "binpatch";
65:
66: export type DeltaResult = {
67: sha256: string;
68: patchBytes: number;
69: chainLength: number;
70: };
71:
72: const log = logger.withTag("delta-upgrade");
73:
74: function getPrimaryUpgradeSource(): UpgradeSource {
75: if (!PRIMARY_UPGRADE_SOURCE) {
76: throw new Error("No primary upgrade source is configured");
77: }
78: return PRIMARY_UPGRADE_SOURCE;
79: }
80:
81: const instrument: InstrumentHook = (name, fn) =>
82: withTracing(name, "http.client", fn);
83:
84: function ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/src/lib/delta-upgrade.ts</path>
<type>file</type>
<content>
425: event.type === "bytes" &&
426: (progress === undefined || phase !== event.phase)
427: ) {
428: // New phase: spin up a fresh bar. The apply phase totals bytes across
429: // every hop's `newSize`, which for multi-hop chains far exceeds the
430: // final binary (e.g. 930 MB shown for a 310 MB install). Switch to
431: // percent-only rendering so users see a sane progress fraction rather
432: // than a scary inflated byte count. Pre-apply ("download"/"read")
433: // phases still show bytes since their totals are honest sizes.
434: phase = event.phase;
435: previousWritten = 0;
436: const isApply = event.phase === "apply";
437: progress = makeByteProgress(
438: `${isApply ? ...
diff --git a/packages/cli/test/lib/binary.test.ts b/packages/cli/test/lib/binary.test.ts
index 6fcfb3b1a..8f49abb41 100644
--- a/packages/cli/test/lib/binary.test.ts
+++ b/packages/cli/test/lib/binary.test.ts
@@ -1,143 +1,267 @@
/**
* Binary Management Tests
*
* Tests for shared binary helpers: install directory selection, paths,
* download URLs, locking, and binary installation.
*/
import {
chmodSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { access, readFile, writeFile } from "node:fs/promises";
import { join, sep } from "node:path";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
acquireLock,
compareVersions,
determineInstallDir,
fetchWithUpgradeError,
getBinaryDownloadUrl,
getBinaryFilename,
getBinaryPaths,
+ getGitHubReleaseByTagUrl,
getLegacyInstallDirs,
getPlatformBinaryName,
installBinary,
isDowngrade,
...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/install</path>
<type>file</type>
<content>
235: source_ghcr_repo() {
236: printf '%s' "$1" | cut -d'|' -f2
237: }
238:
239: # Select the first source whose GitHub probe succeeds. A 404 alone advances to
240: # the next source. The successful source remains fixed for all later requests.
241: select_nightly_source() {
242: local source
243: local manifest_status
244: local github_repo
245: local ghcr_repo
246: local url
247: for source in "${UPGRADE_SOURCES[@]}"; do
248: github_repo=$(source_github_repo "$source")
249: ghcr_repo=$(source_ghcr_repo "$source")
250: url="https://api.github.com/repos/${github_repo}"
251: github_get "$url"
252: if [[ "$http_status" == "404" ]]; then
253: continue
254: fi
255: if [[ ! ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade/packages/cli/install</path>
<type>file</type>
<content>
190: echo -e "${RED}Run: apk add libstdc++ libgcc${NC}" >&2
191: die "Cannot install dependencies without root. Run as root or install manually." "deps"
192: fi
193: fi
194: fi
195: fi
196: fi
197:
198: # Validate supported combinations
199: suffix=""
200: if [[ "$os" == "windows" ]]; then
201: suffix=".exe"
202: if [[ "$arch" != "x64" ]]; then
203: die "Unsupported: windows-$arch (only windows-x64 is supported)" "detect-arch"
204: fi
205: fi
206:
207: # Download binary to a temp location
208: tmpdir="${TMPDIR:-${TMP:-${TEMP:-/tmp}}}"
209: tmp_binary="${tmpdir}/sentry-install-$$${suffix}"
210: github_response="${tmpdir}/sentry-install-github-response-$$"
211: nightly_manifest_file="${tmpdir}/sentry-install-nightly-manifest-$$"
212: version=""
213:
214: # Clean up temp ...
diff --git a/packages/cli/test/lib/delta-upgrade.test.ts b/packages/cli/test/lib/delta-upgrade.test.ts
index 3752d4ae6..7375fd9ec 100644
--- a/packages/cli/test/lib/delta-upgrade.test.ts
+++ b/packages/cli/test/lib/delta-upgrade.test.ts
@@ -1,110 +1,119 @@
/**
* Unit Tests for Delta Upgrade Module
*
* Tests the exported pure-computation functions that drive chain resolution
* for both stable (GitHub Releases) and nightly (GHCR) channels, plus
* async orchestration functions tested via fetch mocking.
*/
import { createHash } from "node:crypto";
import { existsSync, unlinkSync } from "node:fs";
import { access, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
-import { afterEach, beforeEach, describe, expect, test } from "vitest";
-import { getPlatformBinaryName } from "../../src/lib/binary.js";
+import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
+import {
+ getPlatformBinaryName,
+ ...
Date: Sep 9, 2026 * 🔴 (13:49) User established that trailing-separator normalization must strip a trailing separator but never from a bare root like `/`, so entries such as `~/.local/bin/` compare equal to `~/.local/bin`. * 🔴 (13:49) User established that source resolution returns the successful probe response so the caller never repeats the request. * 🟡 (13:49) `packages/cli/src/lib/binary.ts…
Date: Sep 9, 2026 * 🔴 [requested-security-review] (13:46) User requested a strictly read-only security and abuse-case review of getsentry/cli PR #1569 at exact base `bd065083c` and exact head `4094ae9a7`, using worktree `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/toolkit-bridge-upgrade`. * 🔴 (13:46) User required inspection of every changed file and releva…