From 678517f56c322bdf2e55c8e380d9004ad8a80e65 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 19:29:33 +0900 Subject: [PATCH 01/23] release: v2.33.0-preview.20260825 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b3d03972eb..ba2b3ad83c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.1-preview.20260825", + "version": "2.33.0-preview.20260825", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 809a06ba00340c905dfac4ab588616e638c2fbfd Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Thu, 27 Aug 2026 21:33:19 +0900 Subject: [PATCH 02/23] release: v2.34.0-preview.20260827 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd0601a9fcd..f13e70a612f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.34.0", + "version": "2.34.0-preview.20260827", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From dc1feaf7fd8f27c9cc416a1bcbd7ecc247962f2e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Sat, 29 Aug 2026 00:29:18 +0900 Subject: [PATCH 03/23] release: v2.36.0-preview.20260829 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 57f06ad22b3..dc4e2ad765d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.35.0", + "version": "2.36.0-preview.20260829", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 3224168846f4e7b784beffb576f7ecce42308511 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 2 Sep 2026 18:43:29 +0900 Subject: [PATCH 04/23] fix(release): pass the bump job's permissions through the reusable-workflow call (#3262) Both v2.40.0 release dispatches (33615174183 preview, 33615177849 main) died at startup_failure: a workflow_call cannot grant its callee more than the calling job holds, and dev-version-bump.yml's job declares contents+pull- requests write. #3129 wired the call but never dispatched a release, so this is its first live run. The caller job now declares exactly the callee's two permissions; no other job in release.yml gains anything. Co-authored-by: jun (cherry picked from commit 7ce0ba51834740d7b4d5ec4793f6572d84624409) --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 458bb67e0a5..261aece1d18 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,14 @@ jobs: bump-dev-version: needs: publish if: ${{ inputs.dry-run != true }} + # A reusable-workflow CALL cannot grant the callee more than the calling job holds, + # and GitHub refuses the whole run at startup when the called workflow's own job + # declares permissions the caller did not pass down ("startup_failure", runs + # 33615174183 / 33615177849 — the first dispatches since #3129 wired this call). + # The callee's job declares exactly these two; nothing else in this file gains them. + permissions: + contents: write + pull-requests: write uses: ./.github/workflows/dev-version-bump.yml with: released-version: v${{ inputs.version }} From 954b99d7bf3395f23ee186d402c43d32824de227 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Tue, 8 Sep 2026 17:44:26 +0900 Subject: [PATCH 05/23] release: set preview channel version 2.48.0-preview.20260908 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e2d2f3fe8ea..95794a7f46f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.47.0-preview.20260908", + "version": "2.48.0-preview.20260908", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From d24ff57bc4dd53afbbb1d2c972266e6d89ea5c24 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Tue, 8 Sep 2026 17:44:26 +0900 Subject: [PATCH 06/23] release: set main channel version 2.48.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7d94d23cab8..6547da65528 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.47.0", + "version": "2.48.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 02044b294b66de3d5799a5ff84e17c3230c8246c Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 14 Sep 2026 19:28:06 +0900 Subject: [PATCH 07/23] chore(release): promote 2.55.0-preview.20260914 to preview Promotes the dev product snapshot 62f02223a0 to the preview train. The 2.55.0 line carries the #4546 cost-guard work: one send budget per logical request with a shared final-recovery reserve, zero-is-zero refusals with a typed error rather than a synthetic 502, compact and the Kiro inner retries admitted against that budget, a finite send ceiling per root workflow with an interactive reserve a fan-out cannot take, and a healthy detour promoted on transient-hold expiry instead of released cold. The previous preview tip 2.54.0-preview.20260914 is already tagged and published and is outranked by v2.54.0, so it could not be re-released; this is a new candidate rather than a re-cut. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e9f74ae75a..fdcec0ec5d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.55.0", + "version": "2.55.0-preview.20260914", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 58c15b819a1f9bc5611e0387ce5e62b7de82bcf6 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 14 Sep 2026 19:47:46 +0900 Subject: [PATCH 08/23] chore(release): promote the verified 2.55.0 product tree to main Same product tree as preview 7bdd1b29b5 / 2.55.0-preview.20260914, which published successfully with its registry smoke green. Only package.json version differs. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fdcec0ec5d3..9e9f74ae75a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.55.0-preview.20260914", + "version": "2.55.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 9bc8acbb8c1fd6a7adfa4f8ed8622ad2b814b62a Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 19:31:52 +0900 Subject: [PATCH 09/23] ci(release): expose Linux bundler diagnostics for stable artifacts --- .github/workflows/release.yml | 4 +++- structure/ops/docs-and-release.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e16f4ecbe1f..9a62e7aad67 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -351,7 +351,9 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} MACOS_SIGN_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: bunx tauri build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} + # linuxdeploy suppresses its own stderr at the default verbosity. Keep + # diagnostics on the first attempt; Apple signing commands stay non-verbose. + run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} - name: Rename release assets shell: bash diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index fc183d8f1a6..9fc4dcfa50b 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -520,3 +520,5 @@ Malformed or unreadable records remain unknown. Recovery requires the same compl identity and proven-dead liveness; unknown or transferred ownership never starts another proxy. Direct recovery retains the lease until readiness or its bounded deadline. The normal successful manual-runtime update still prints the existing restart hint. + +Linux release bundling enables Tauri verbosity on the primary attempt so linuxdeploy diagnostics remain visible. macOS signing verbosity and publication/signature gates are unchanged. From 5a7c48c5809d4fdfa626fd68187f1b6de92923ae Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 19:47:05 +0900 Subject: [PATCH 10/23] fix(release): prepare stable platform bundles --- .github/workflows/release.yml | 10 ++++++- desktop/scripts/windows-installer-config.ts | 28 ++++++++++++++++++ structure/ops/docs-and-release.md | 2 ++ .../release-desktop-scripts.test.ts | 29 ++++++++++++++++++- 4 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 desktop/scripts/windows-installer-config.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a62e7aad67..8bb202733b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -228,6 +228,7 @@ jobs: uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master with: toolchain: stable + targets: ${{ runner.os == 'macOS' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Install Linux desktop dependencies if: runner.os == 'Linux' @@ -336,6 +337,13 @@ jobs: echo "$description" | grep -q "flags=.*runtime" echo "$description" | grep -q "Timestamp=" + - name: Prepare Windows installer version + if: runner.os == 'Windows' + shell: bash + env: + RELEASE_VERSION: ${{ inputs.version }} + run: bun desktop/scripts/windows-installer-config.ts "$RELEASE_VERSION" "$RUNNER_TEMP/opencodex-msi.json" + # Release signing is intentionally secret-gated. Developer ID, notarization, # and updater signatures require maintainer-owned credentials; builds without # those secrets remain useful for local validation but are not release assets. @@ -353,7 +361,7 @@ jobs: MACOS_SIGN_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} # linuxdeploy suppresses its own stderr at the default verbosity. Keep # diagnostics on the first attempt; Apple signing commands stay non-verbose. - run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} + run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config "${{ runner.os == 'Windows' && format('{0}/opencodex-msi.json', runner.temp) || '{}' }}" - name: Rename release assets shell: bash diff --git a/desktop/scripts/windows-installer-config.ts b/desktop/scripts/windows-installer-config.ts new file mode 100644 index 00000000000..0a5f64c2ce8 --- /dev/null +++ b/desktop/scripts/windows-installer-config.ts @@ -0,0 +1,28 @@ +import { writeFileSync } from "node:fs"; + +/** MSI cannot express SemVer prerelease precedence. Keep public application and + * updater versions intact and override only WiX ProductVersion with the core. + * The pinned Tauri template permits equal-core replacement; manual MSI installs + * therefore do not prevent same-core channel downgrades. */ +export function windowsInstallerVersion(version: string): string { + if (version.length > 128) throw new Error("Public version exceeds the installer metadata bound"); + const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(version); + if (!match || match[4]?.split(".").some(part => /^\d+$/.test(part) && part.length > 1 && part[0] === "0")) { + throw new Error("A valid public SemVer is required for the Windows installer"); + } + const parts = [match[1]!, match[2]!, match[3]!].map(Number); + if (parts.some((part, index) => !Number.isSafeInteger(part) || part > (index < 2 ? 255 : 65_535))) { + throw new Error("Windows installer version exceeds MSI numeric limits"); + } + return parts.join("."); +} + +export function windowsInstallerConfig(version: string) { + return { bundle: { windows: { wix: { version: windowsInstallerVersion(version) } } } }; +} + +if (import.meta.main) { + const [version, output] = process.argv.slice(2); + if (!version || !output) throw new Error("Usage: windows-installer-config "); + writeFileSync(output, `${JSON.stringify(windowsInstallerConfig(version))}\n`, { mode: 0o600 }); +} diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 9fc4dcfa50b..5495a6728f0 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -522,3 +522,5 @@ Direct recovery retains the lease until readiness or its bounded deadline. The n manual-runtime update still prints the existing restart hint. Linux release bundling enables Tauri verbosity on the primary attempt so linuxdeploy diagnostics remain visible. macOS signing verbosity and publication/signature gates are unchanged. + +Universal macOS release builds install both aarch64-apple-darwin and x86_64-apple-darwin Rust targets. Windows builds consume the private JSON override generated by `desktop/scripts/windows-installer-config.ts`: only WiX ProductVersion uses the validated numeric public version core. Public package/application versions, tags, asset names and updater manifests retain full SemVer. The pinned Tauri MSI template permits equal-core replacement; manual MSI installation does not enforce same-core preview/stable downgrade prevention. diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index b1e865529d4..891e1387f42 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -1,3 +1,4 @@ +import { windowsInstallerConfig, windowsInstallerVersion } from "../../desktop/scripts/windows-installer-config"; import { describe, expect, test } from "bun:test"; import { createHash, generateKeyPairSync, sign as ed25519Sign } from "node:crypto"; import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; @@ -27,6 +28,17 @@ function temporaryDirectory(): string { } describe("desktop release scripts", () => { + test("MSI uses numeric core while public SemVer metadata remains external", () => { + for (const version of ["2.61.0", "2.61.0-preview.20260922", "2.61.0-preview.20260922.1+build.7"]) { + expect(windowsInstallerVersion(version)).toBe("2.61.0"); + expect(windowsInstallerConfig(version)).toEqual({ bundle: { windows: { wix: { version: "2.61.0" } } } }); + } + expect(windowsInstallerVersion("255.255.65535")).toBe("255.255.65535"); + for (const version of ["256.1.0", "1.256.0", "1.1.65536", "2.01.0", "2.1.0-01", "v2.1.0", "2.1", "2.1.0;evil", "999999999999999999.0.0"]) { + expect(() => windowsInstallerVersion(version)).toThrow(); + } + }); + test("renames macOS DMG and updater archive and copies signatures", () => { const root = temporaryDirectory(); try { @@ -405,7 +417,7 @@ describe("widget extension signing", () => { ) as { jobs?: Record; - steps?: Array<{ name?: string; if?: string; run?: string; env?: Record }>; + steps?: Array<{ name?: string; if?: string; run?: string; env?: Record; with?: Record }>; }>; }; const steps = workflow.jobs?.["package-desktop"]?.steps ?? []; @@ -417,6 +429,21 @@ describe("widget extension signing", () => { const indexOfStepRunning = (fragment: string) => steps.findIndex(step => typeof step.run === "string" && step.run.includes(fragment)); + test("release prepares both Mac architectures and wires only the MSI metadata override", () => { + const rust = steps.find(step => step.name === "Setup Rust"); + expect(rust?.with?.targets).toContain("aarch64-apple-darwin,x86_64-apple-darwin"); + expect(rust?.with?.targets).toContain("runner.os == 'macOS'"); + const prepare = steps.find(step => step.name === "Prepare Windows installer version"); + expect(prepare?.if).toBe("runner.os == 'Windows'"); + expect(prepare?.env?.RELEASE_VERSION).toBe("${{ inputs.version }}"); + expect(prepare?.run).toContain('windows-installer-config.ts "$RELEASE_VERSION" "$RUNNER_TEMP/opencodex-msi.json"'); + const build = steps.find(step => step.name === "Build desktop bundles"); + expect(build?.run).toContain("--config"); + expect(build?.run).toContain("format('{0}/opencodex-msi.json', runner.temp)"); + expect(build?.run).toContain("runner.os == 'Windows'"); + expect(indexOfStep("Prepare Windows installer version")).toBeLessThan(indexOfStep("Build desktop bundles")); + }); + test("the release build hands the widget a signing identity and forbids an ad-hoc fallback", () => { const build = steps.find(step => step.name === "Build WidgetKit extension"); expect(build).toBeDefined(); From 5aa92e9548144fcc18ca3d3c986c41fd72c988a0 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 20:13:53 +0900 Subject: [PATCH 11/23] fix(desktop): carry native sidecar packaging repair to main (#5532) --- .github/workflows/release.yml | 14 ++++++++++ desktop/scripts/appimage-patchelf.py | 26 +++++++++++++++++++ desktop/scripts/verify-linux-sidecar.sh | 22 ++++++++++++++++ structure/desktop-shell.md | 9 +++++++ .../release-desktop-scripts.test.ts | 16 ++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 desktop/scripts/appimage-patchelf.py create mode 100644 desktop/scripts/verify-linux-sidecar.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8bb202733b0..72a02821694 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -245,6 +245,10 @@ jobs: run: | bun desktop/scripts/prepare-sidecar.ts --target aarch64-apple-darwin bun desktop/scripts/prepare-sidecar.ts --target x86_64-apple-darwin + lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin \ + desktop/src-tauri/binaries/ocx-x86_64-apple-darwin \ + -output desktop/src-tauri/binaries/ocx-universal-apple-darwin + lipo -verify_arch arm64 x86_64 desktop/src-tauri/binaries/ocx-universal-apple-darwin - name: Prepare sidecar if: runner.os != 'macOS' @@ -344,6 +348,12 @@ jobs: RELEASE_VERSION: ${{ inputs.version }} run: bun desktop/scripts/windows-installer-config.ts "$RELEASE_VERSION" "$RUNNER_TEMP/opencodex-msi.json" + - name: Preserve the compiled Linux sidecar + if: runner.os == 'Linux' + run: | + chmod +x desktop/scripts/appimage-patchelf.py + echo "PATCHELF=$GITHUB_WORKSPACE/desktop/scripts/appimage-patchelf.py" >> "$GITHUB_ENV" + # Release signing is intentionally secret-gated. Developer ID, notarization, # and updater signatures require maintainer-owned credentials; builds without # those secrets remain useful for local validation but are not release assets. @@ -363,6 +373,10 @@ jobs: # diagnostics on the first attempt; Apple signing commands stay non-verbose. run: bunx tauri ${{ runner.os == 'Linux' && '--verbose' || '' }} build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config "${{ runner.os == 'Windows' && format('{0}/opencodex-msi.json', runner.temp) || '{}' }}" + - name: Verify the packaged Linux sidecar + if: runner.os == 'Linux' + run: bash desktop/scripts/verify-linux-sidecar.sh + - name: Rename release assets shell: bash env: diff --git a/desktop/scripts/appimage-patchelf.py b/desktop/scripts/appimage-patchelf.py new file mode 100644 index 00000000000..4c7e73cd930 --- /dev/null +++ b/desktop/scripts/appimage-patchelf.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Keep the compiled Bun sidecar intact while linuxdeploy patches the host/libs.""" +import os +from pathlib import Path +import sys + + +def main(args): + root = Path(__file__).resolve().parents[2] + triple = "x86_64-unknown-linux-gnu" + original = root / "desktop/src-tauri/binaries" / f"ocx-{triple}" + sidecar = root / "desktop/src-tauri/target" / triple / "release/bundle/appimage/OpenCodex.AppDir/usr/bin/ocx" + if len(args) == 3 and args[:2] == ["--set-rpath", "$ORIGIN/../lib"] and Path(args[2]).resolve() == sidecar.resolve(): + # linuxdeploy's nested GTK pass runs ldd again after patching. Its + # patchelf rewrite breaks the compiled Bun ELF. This sidecar depends + # only on host glibc libraries; it needs no AppDir library search path. + # Never bless an already-modified binary or a different executable. + if sidecar.is_symlink() or original.read_bytes() != sidecar.read_bytes(): + raise RuntimeError("AppImage sidecar differs from the prepared CLI") + print("Preserving compiled ocx bytes (no AppDir RPATH required)", file=sys.stderr) + return + os.execv("/usr/bin/patchelf", ["/usr/bin/patchelf", *args]) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/desktop/scripts/verify-linux-sidecar.sh b/desktop/scripts/verify-linux-sidecar.sh new file mode 100644 index 00000000000..88c5df2ba34 --- /dev/null +++ b/desktop/scripts/verify-linux-sidecar.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Run only on a Linux packaging runner, against the completed AppImage. +set -euo pipefail +root="$(cd "$(dirname "$0")/../.." && pwd)" +bundle="$root/desktop/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage" +original="$root/desktop/src-tauri/binaries/ocx-x86_64-unknown-linux-gnu" +shopt -s nullglob +images=("$bundle"/*.AppImage) +if [ "${#images[@]}" -ne 1 ]; then + echo "Expected exactly one completed AppImage" >&2 + exit 1 +fi +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT +cd "$scratch" +"${images[0]}" --appimage-extract > /dev/null +sidecar="$scratch/squashfs-root/usr/bin/ocx" +test ! -L "$sidecar" +cmp "$original" "$sidecar" +sha256sum "$original" "$sidecar" +mkdir "$scratch/home" +timeout 30s env OPENCODEX_HOME="$scratch/home" "$sidecar" --version diff --git a/structure/desktop-shell.md b/structure/desktop-shell.md index 0bf23cc96fe..e967bf04d55 100644 --- a/structure/desktop-shell.md +++ b/structure/desktop-shell.md @@ -195,6 +195,15 @@ marker, which the GUI detects to identify the shell without using IPC. ## Release packaging and updater +Linux AppImage packaging uses `desktop/scripts/appimage-patchelf.py` to preserve +the compiled Bun CLI when linuxdeploy sets the executable RPATH. Only the exact +AppDir sidecar, still byte-identical to the prepared CLI, is exempt; other ELF +operations use the system patchelf. `desktop/scripts/verify-linux-sidecar.sh` +extracts the completed AppImage, compares its CLI bytes and runs its version command +on the hosted runner before any release asset is collected. +The macOS release combines both prepared CLI architectures with `lipo` into the +universal external binary Tauri expects, and checks that both slices are present. + The release workflow packages the desktop shell as `OpenCodex--macos.dmg`, `OpenCodex--windows-x64.msi`, `OpenCodex--linux-x86_64.AppImage`, and `OpenCodex--linux-amd64.deb`. Each artifact is collected with a `.sha256` file; diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index 891e1387f42..ac4d971b2bb 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -433,6 +433,10 @@ describe("widget extension signing", () => { const rust = steps.find(step => step.name === "Setup Rust"); expect(rust?.with?.targets).toContain("aarch64-apple-darwin,x86_64-apple-darwin"); expect(rust?.with?.targets).toContain("runner.os == 'macOS'"); + const sidecars = steps.find(step => step.name === "Prepare macOS sidecars"); + expect(sidecars?.run).toContain("lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin"); + expect(sidecars?.run).toContain("-output desktop/src-tauri/binaries/ocx-universal-apple-darwin"); + expect(sidecars?.run).toContain("lipo -verify_arch arm64 x86_64"); const prepare = steps.find(step => step.name === "Prepare Windows installer version"); expect(prepare?.if).toBe("runner.os == 'Windows'"); expect(prepare?.env?.RELEASE_VERSION).toBe("${{ inputs.version }}"); @@ -444,6 +448,18 @@ describe("widget extension signing", () => { expect(indexOfStep("Prepare Windows installer version")).toBeLessThan(indexOfStep("Build desktop bundles")); }); + test("Linux verifies the packaged CLI before collecting release assets", () => { + const preserve = steps.find(step => step.name === "Preserve the compiled Linux sidecar"); + const verify = steps.find(step => step.name === "Verify the packaged Linux sidecar"); + expect(preserve?.if).toBe("runner.os == 'Linux'"); + expect(preserve?.run).toContain("PATCHELF=$GITHUB_WORKSPACE/desktop/scripts/appimage-patchelf.py"); + expect(verify?.if).toBe("runner.os == 'Linux'"); + expect(verify?.run).toBe("bash desktop/scripts/verify-linux-sidecar.sh"); + expect(indexOfStep(preserve!.name!)).toBeLessThan(indexOfStep("Build desktop bundles")); + expect(indexOfStep(verify!.name!)).toBeGreaterThan(indexOfStep("Build desktop bundles")); + expect(indexOfStep(verify!.name!)).toBeLessThan(indexOfStep("Rename release assets")); + }); + test("the release build hands the widget a signing identity and forbids an ad-hoc fallback", () => { const build = steps.find(step => step.name === "Build WidgetKit extension"); expect(build).toBeDefined(); From 9314146ae88aefe5b6ce780ef6f4d7a7c460f330 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 20:22:21 +0900 Subject: [PATCH 12/23] chore(ci): refresh main release verification (#5534) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8f86a91e6e..753a72c5fcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ on: permissions: contents: read -# Retrigger CI after dir-fsync / oauth deadline follow-ups (tip 34a1ac46). +# Release promotions require fresh push-based CI evidence for the published commit. concurrency: # `push` and `pull_request` want supersession: a newer head on the same ref # makes the older one irrelevant, and cancelling it saves an hour of runners From b007d339b95ad8308632548564e15c8844238f72 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 20:28:20 +0900 Subject: [PATCH 13/23] fix(release): carry lipo argument fix to main (#5537) --- .github/workflows/release.yml | 2 +- tests/ci-workflows/release-desktop-scripts.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72a02821694..5adedd5cac8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -248,7 +248,7 @@ jobs: lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin \ desktop/src-tauri/binaries/ocx-x86_64-apple-darwin \ -output desktop/src-tauri/binaries/ocx-universal-apple-darwin - lipo -verify_arch arm64 x86_64 desktop/src-tauri/binaries/ocx-universal-apple-darwin + lipo desktop/src-tauri/binaries/ocx-universal-apple-darwin -verify_arch arm64 x86_64 - name: Prepare sidecar if: runner.os != 'macOS' diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index ac4d971b2bb..416895f6845 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -436,7 +436,7 @@ describe("widget extension signing", () => { const sidecars = steps.find(step => step.name === "Prepare macOS sidecars"); expect(sidecars?.run).toContain("lipo -create desktop/src-tauri/binaries/ocx-aarch64-apple-darwin"); expect(sidecars?.run).toContain("-output desktop/src-tauri/binaries/ocx-universal-apple-darwin"); - expect(sidecars?.run).toContain("lipo -verify_arch arm64 x86_64"); + expect(sidecars?.run).toContain("lipo desktop/src-tauri/binaries/ocx-universal-apple-darwin -verify_arch arm64 x86_64"); const prepare = steps.find(step => step.name === "Prepare Windows installer version"); expect(prepare?.if).toBe("runner.os == 'Windows'"); expect(prepare?.env?.RELEASE_VERSION).toBe("${{ inputs.version }}"); From b1ae178ec5b3cb7bdd02cca04da08a6210415db4 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 21:21:46 +0900 Subject: [PATCH 14/23] fix(release): carry checksum and signature repair to main (#5546) --- .github/workflows/release.yml | 4 +- desktop/scripts/verify-release-assets.ts | 66 +++++++++++------ structure/desktop-shell.md | 3 + .../release-desktop-scripts.test.ts | 71 +++++++++++++++++-- .../release-pipeline-contract.test.ts | 3 + tests/fixtures/minisign/LICENSE | 60 ++++++++++++++++ tests/fixtures/minisign/prehashed-vector.json | 6 ++ 7 files changed, 186 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/minisign/LICENSE create mode 100644 tests/fixtures/minisign/prehashed-vector.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5adedd5cac8..e62765c5358 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,8 +165,8 @@ jobs: # dist/release, where the artifact download lands these files flat; the # checksum therefore records the bare file name, which sha256sum takes # verbatim from its argument. - if [[ "$RUNNER_OS" == "Windows" ]]; then sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256" - else sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256" + if [[ "$RUNNER_OS" == "Windows" ]]; then sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip.sha256" + else sha256sum "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" > "ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz.sha256" fi - name: Upload standalone release diff --git a/desktop/scripts/verify-release-assets.ts b/desktop/scripts/verify-release-assets.ts index 7292be7a684..06461d92c80 100644 --- a/desktop/scripts/verify-release-assets.ts +++ b/desktop/scripts/verify-release-assets.ts @@ -129,22 +129,36 @@ export interface MinisignPublicKey { publicKey: KeyObject; } -function minisignPayload(text: string, expectedBytes: number, what: string): Buffer { - const encoded = text - .split("\n") - .filter(line => line.trim().length > 0 && !line.trimStart().startsWith("untrusted comment:")) - .join("") - .trim(); - const payload = Buffer.from(encoded, "base64"); - if (payload.length !== expectedBytes) { - throw new Error(`Malformed ${what}: expected ${expectedBytes} decoded bytes, got ${payload.length}`); +function decodeBase64(text: string, what: string, expectedBytes?: number): Buffer { + const payload = Buffer.from(text, "base64"); + // Buffer.from is intentionally permissive; release metadata must be canonical. + if (!text || payload.toString("base64") !== text + || (expectedBytes !== undefined && payload.length !== expectedBytes)) { + throw new Error(`Malformed ${what}: invalid base64 or decoded length`); } return payload; } +function decodeBox(text: string, what: string): string { + // Transport whitespace is harmless (the updater manifest also trims it). + // The encoded payload itself must still be canonical and valid UTF-8. + const payload = decodeBase64(text.trim(), what); + return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(payload); +} + +function boxLines(text: string): string[] { + // Accept minisign text with LF or CRLF and an optional terminal newline; + // signatures authenticate decoded bytes/comments, not transport line endings. + return text.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n"); +} + /** minisign public key: base64 of algorithm ("Ed") || key id (8) || raw key (32). */ export function parseMinisignPublicKey(text: string): MinisignPublicKey { - const payload = minisignPayload(text, 42, "minisign public key"); + const lines = boxLines(text); + if (lines.length !== 2 || !lines[0]!.startsWith("untrusted comment: ")) { + throw new Error("Malformed minisign public key box"); + } + const payload = decodeBase64(lines[1]!, "minisign public key", 42); const algorithm = payload.subarray(0, 2).toString("utf8"); if (algorithm !== "Ed") { throw new Error(`Unsupported minisign public key algorithm: ${JSON.stringify(algorithm)}`); @@ -166,30 +180,42 @@ export function loadUpdaterPublicKey(tauriConfPath: string): MinisignPublicKey { }; const pubkey = conf.plugins?.updater?.pubkey; if (!pubkey) throw new Error(`No plugins.updater.pubkey in ${tauriConfPath}`); - return parseMinisignPublicKey(Buffer.from(pubkey, "base64").toString("utf8")); + return parseMinisignPublicKey(decodeBox(pubkey, "Tauri public key")); } -/** - * minisign signature: base64 of algorithm || key id (8) || signature (64). - * "Ed" is a pure Ed25519 signature over the raw file bytes — the form the Tauri - * bundler emits. "ED" (BLAKE2b-prehashed) or anything else fails loudly rather - * than being silently mis-verified. - */ +/** Tauri CLI 2.11.1 wraps a minisign 0.7.3 prehashed signature box in base64. */ export function verifyUpdaterSignature(filePath: string, key: MinisignPublicKey): void { const signaturePath = `${filePath}.sig`; if (!existsSync(signaturePath)) throw new Error(`Missing signature: ${signaturePath}`); - const payload = minisignPayload(readFileSync(signaturePath, "utf8"), 74, `signature ${signaturePath}`); + const lines = boxLines(decodeBox(readFileSync(signaturePath, "utf8"), "Tauri signature")); + const trustedPrefix = "trusted comment: "; + if (lines.length !== 4 || !lines[0]!.startsWith("untrusted comment: ") + || !lines[2]!.startsWith(trustedPrefix)) { + throw new Error(`Malformed signature box in ${signaturePath}`); + } + const payload = decodeBase64(lines[1]!, "signature packet", 74); + const globalSignature = decodeBase64(lines[3]!, "comment signature", 64); const algorithm = payload.subarray(0, 2).toString("utf8"); - if (algorithm !== "Ed") { + if (algorithm !== "ED") { throw new Error(`Unsupported signature algorithm in ${signaturePath}: ${JSON.stringify(algorithm)}`); } const keyId = payload.subarray(2, 10).toString("hex"); if (keyId !== key.keyId) { throw new Error(`Signature ${signaturePath} was made by key ${keyId}, not the pinned updater key ${key.keyId}`); } - if (!ed25519Verify(null, readFileSync(filePath), key.publicKey, payload.subarray(10, 74))) { + const signature = payload.subarray(10, 74); + // ED is ordinary Ed25519 over the BLAKE2b-512 digest, not Ed25519ph. + const digest = createHash("blake2b512").update(readFileSync(filePath)).digest(); + if (!ed25519Verify(null, digest, key.publicKey, signature)) { throw new Error(`Signature verification failed for ${filePath}`); } + // minisign signs the raw signature + trimmed trusted comment, without its + // prefix or line terminator. The original filename may differ after collection. + const trustedComment = lines[2]!.slice(trustedPrefix.length).trim(); + const message = Buffer.concat([signature, Buffer.from(trustedComment, "utf8")]); + if (!ed25519Verify(null, message, key.publicKey, globalSignature)) { + throw new Error(`Comment signature verification failed for ${filePath}`); + } } function parseBackManifest(manifestPath: string, options: VerifyReleaseAssetsOptions): string[] { diff --git a/structure/desktop-shell.md b/structure/desktop-shell.md index e967bf04d55..66aa77423f3 100644 --- a/structure/desktop-shell.md +++ b/structure/desktop-shell.md @@ -214,6 +214,9 @@ packaging matrices, verifies every checksum and every updater signature, and wri platforms to have updater signatures. Publication waits for that verification, and the attachment job uploads the verified bundle only after the verification receipt names the same version and commit. +Updater signature verification decodes Tauri’s outer-base64 minisign box, checks the +`ED` signature over the BLAKE2b-512 digest against the pinned key, and verifies the +trusted-comment signature. Missing or malformed fields fail before publication. On macOS, in-app updates download `OpenCodex--macos.app.tar.gz`; the DMG is for the first installation. diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index 416895f6845..dad055ce31c 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -530,17 +530,44 @@ describe("release asset verification", () => { function makeMinisignKeypair(keyIdHex: string): { pubkeyText: string; keyId: Buffer; - signPayload: (payload: Buffer) => string; + signPayload: (payload: Buffer, rawBytes?: boolean) => string; } { const { publicKey, privateKey } = generateKeyPairSync("ed25519"); const raw = Buffer.from(publicKey.export({ format: "der", type: "spki" })).subarray(-32); const keyId = Buffer.from(keyIdHex, "hex"); const pubkeyText = `untrusted comment: test public key\n${Buffer.concat([Buffer.from("Ed"), keyId, raw]).toString("base64")}\n`; - const signPayload = (payload: Buffer): string => - `untrusted comment: test signature\n${Buffer.concat([Buffer.from("Ed"), keyId, ed25519Sign(null, payload, privateKey)]).toString("base64")}\n`; + const signPayload = (payload: Buffer, rawBytes = false): string => { + const signed = rawBytes ? payload : createHash("blake2b512").update(payload).digest(); + const signature = ed25519Sign(null, signed, privateKey); + const trusted = "timestamp:1\tfile:original-before-collection.msi"; + const packet = Buffer.concat([Buffer.from("ED"), keyId, signature]).toString("base64"); + const global = ed25519Sign(null, Buffer.concat([signature, Buffer.from(trusted)]), privateKey).toString("base64"); + return Buffer.from(`untrusted comment: test signature\n${packet}\ntrusted comment: ${trusted}\n${global}\n`).toString("base64"); + }; return { pubkeyText, keyId, signPayload }; } + test("accepts the upstream minisign 0.7.3 prehashed vector in Tauri encoding", () => { + const fixture = JSON.parse(readFileSync(repoPath("tests/fixtures/minisign/prehashed-vector.json"), "utf8")) as { + payload: string; publicKeyBase64: string; signatureBox: string; + }; + const dir = temporaryDirectory(); + try { + const asset = join(dir, "renamed-release.bin"); + const key = parseMinisignPublicKey(`untrusted comment: upstream key\n${fixture.publicKeyBase64}\n`); + writeFileSync(asset, fixture.payload); + writeFileSync(`${asset}.sig`, Buffer.from(fixture.signatureBox).toString("base64")); + expect(() => verifyUpdaterSignature(asset, key)).not.toThrow(); + writeFileSync(asset, "tampered"); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Signature verification failed/); + writeFileSync(asset, fixture.payload); + writeFileSync(`${asset}.sig`, Buffer.from(fixture.signatureBox.replace("file:test", "file:changed")).toString("base64")); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Comment signature verification failed/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + test("derives the expected set from the real release matrices and producer tables", () => { const workflow = readFileSync(repoPath(".github", "workflows", "release.yml"), "utf8"); const { standaloneTargets: workflowStandalone, desktopTargets } = releaseMatrixTargets(workflow); @@ -624,9 +651,43 @@ describe("release asset verification", () => { writeFileSync(`${asset}.sig`, other.signPayload(payload)); expect(() => verifyUpdaterSignature(asset, key)).toThrow(/not the pinned updater key/); - const hashed = `untrusted comment: test\n${Buffer.concat([Buffer.from("ED"), other.keyId, Buffer.alloc(64)]).toString("base64")}\n`; - writeFileSync(`${asset}.sig`, hashed); + const valid = signPayload(payload); + writeFileSync(`${asset}.sig`, signPayload(payload, true)); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Signature verification failed/); + const lines = Buffer.from(valid, "base64").toString("utf8").trimEnd().split("\n"); + const encode = (box: string[]) => Buffer.from(`${box.join("\n")}\n`).toString("base64"); + const packet = Buffer.from(lines[1]!, "base64"); + packet[10] = packet[10]! ^ 1; + writeFileSync(`${asset}.sig`, encode([lines[0]!, packet.toString("base64"), lines[2]!, lines[3]!])); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Signature verification failed/); + packet[10] = packet[10]! ^ 1; + packet.write("Ed", 0); + writeFileSync(`${asset}.sig`, encode([lines[0]!, packet.toString("base64"), lines[2]!, lines[3]!])); expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Unsupported signature algorithm/); + + writeFileSync(`${asset}.sig`, encode([lines[0]!, lines[1]!, "trusted comment: changed", lines[3]!])); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Comment signature verification failed/); + writeFileSync(`${asset}.sig`, encode([lines[0]!, lines[1]!, lines[2]!, Buffer.alloc(64).toString("base64")])); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(/Comment signature verification failed/); + const sameIdOtherKey = parseMinisignPublicKey(makeMinisignKeypair("0123456789abcdef").pubkeyText); + writeFileSync(`${asset}.sig`, valid); + expect(() => verifyUpdaterSignature(asset, sameIdOtherKey)).toThrow(/Signature verification failed/); + + for (const malformed of [valid + "!", encode(lines.slice(0, 3)), encode([...lines, "extra"]), + encode([lines[0]!, Buffer.alloc(73).toString("base64"), lines[2]!, lines[3]!]), + encode([lines[0]!, lines[1]!, lines[2]!, Buffer.alloc(65).toString("base64")]), + Buffer.from([0xff]).toString("base64"), + Buffer.from(`\uFEFF${lines.join("\n")}`).toString("base64"), + lines.slice(0, 2).join("\n")]) { + writeFileSync(`${asset}.sig`, malformed); + expect(() => verifyUpdaterSignature(asset, key)).toThrow(); + } + writeFileSync(`${asset}.sig`, encode(["untrusted comment: changed", ...lines.slice(1)])); + expect(() => verifyUpdaterSignature(asset, key)).not.toThrow(); + writeFileSync(`${asset}.sig`, Buffer.from(`${lines.join("\r\n")}\r\n`).toString("base64")); + expect(() => verifyUpdaterSignature(asset, key)).not.toThrow(); + writeFileSync(`${asset}.sig`, ` \n${valid}\n`); + expect(() => verifyUpdaterSignature(asset, key)).not.toThrow(); } finally { rmSync(dir, { recursive: true, force: true }); } diff --git a/tests/ci-workflows/release-pipeline-contract.test.ts b/tests/ci-workflows/release-pipeline-contract.test.ts index 208b1d962c0..8f474ae4b25 100644 --- a/tests/ci-workflows/release-pipeline-contract.test.ts +++ b/tests/ci-workflows/release-pipeline-contract.test.ts @@ -93,6 +93,9 @@ describe("release pipeline contract", () => { const output = />\s+"([^"]+)"/.exec(line)?.[1]; expect(output, line).toBeDefined(); expect(output!).not.toContain("/"); + // verifyChecksums binds each record to its own payload by removing only + // the final .sha256 suffix, including the archive extension in the name. + expect(output, line).toBe(`${argument}.sha256`); } // The bare names above only resolve end to end if the step checksums from the directory diff --git a/tests/fixtures/minisign/LICENSE b/tests/fixtures/minisign/LICENSE new file mode 100644 index 00000000000..19ed8ad543a --- /dev/null +++ b/tests/fixtures/minisign/LICENSE @@ -0,0 +1,60 @@ +Copyright (c) 2019-2023 Frank Denis +Copyright (c) 2017 Daniel Rangel + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +-- + +Code in the src/crypto folder is derived from the rust-crypto project: +https://github.com/DaGenix/rust-crypto + +Original ISC license follows: + +Copyright (c) 2006-2009 Graydon Hoare +Copyright (c) 2009-2013 Mozilla Foundation + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/minisign/prehashed-vector.json b/tests/fixtures/minisign/prehashed-vector.json new file mode 100644 index 00000000000..44cd225d9b0 --- /dev/null +++ b/tests/fixtures/minisign/prehashed-vector.json @@ -0,0 +1,6 @@ +{ + "source": "https://github.com/jedisct1/rust-minisign/blob/068d25a2fffd3d9da96e43f3844c830d8176d5a0/src/tests.rs#L159-L185", + "payload": "test", + "publicKeyBase64": "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3", + "signatureBox": "untrusted comment: signature from minisign secret key\nRUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=\ntrusted comment: timestamp:1556193335\tfile:test\ny/rUw2y8/hOUYjZU71eHp/Wo1KZ40fGy2VJEDl34XMJM+TX48Ss/17u3IvIfbVR1FkZZSNCisQbuQY+bHwhEBg==" +} From f1b218004e5e77c363decd0f59d84b64b4d3f588 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 22:05:50 +0900 Subject: [PATCH 15/23] fix(release): carry Windows checksum record support to main (#5552) --- desktop/scripts/verify-release-assets.ts | 4 +-- .../release-desktop-scripts.test.ts | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/desktop/scripts/verify-release-assets.ts b/desktop/scripts/verify-release-assets.ts index 06461d92c80..0c3175791bb 100644 --- a/desktop/scripts/verify-release-assets.ts +++ b/desktop/scripts/verify-release-assets.ts @@ -95,7 +95,7 @@ export function releaseMatrixTargets(workflowText: string): { /** * Every recorded checksum against the bytes on disk, in exactly the producers' - * format (64 hex, two spaces, bare name, one trailing newline). The recorded name + * format (64 hex, a space, text/binary marker, bare name, newline). The recorded name * must equal the checksum file's own name minus the suffix: a foo.sha256 naming * bar would leave foo's bytes unchecked while bar's are checked twice. */ @@ -104,7 +104,7 @@ export function verifyChecksums(dir: string): number { if (checksumFiles.length === 0) throw new Error(`No .sha256 files found in ${dir}`); for (const checksumFile of checksumFiles) { const content = readFileSync(join(dir, checksumFile), "utf8"); - const match = /^([0-9a-f]{64}) (\S+)\n$/.exec(content); + const match = /^([0-9a-f]{64}) [ *](\S+)\r?\n$/.exec(content); if (!match) throw new Error(`Malformed checksum record in ${checksumFile}: ${JSON.stringify(content)}`); const digest = match[1]!; const recorded = match[2]!; diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index dad055ce31c..037b2f65ad3 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -618,6 +618,35 @@ describe("release asset verification", () => { } }); + test("verifies Windows binary checksum records without weakening payload binding", () => { + const dir = temporaryDirectory(); + const name = "ocx-1.0.0-bun-windows-x64.zip"; + const asset = join(dir, name); + const checksum = `${asset}.sha256`; + // Standard sha256sum binary marker observed in release run 35728908862. + const digest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + try { + writeFileSync(asset, ""); + for (const newline of ["\n", "\r\n"]) { + writeFileSync(checksum, `${digest} *${name}${newline}`); + expect(verifyChecksums(dir)).toBe(1); + } + writeFileSync(checksum, `${digest} *different.zip\n`); + expect(() => verifyChecksums(dir)).toThrow(/must record its own payload/); + for (const record of [`${digest} ?${name}\n`, `${digest}*${name}\n`, `${digest} *${name}\nextra\n`]) { + writeFileSync(checksum, record); + expect(() => verifyChecksums(dir)).toThrow(/Malformed checksum record/); + } + writeFileSync(checksum, `${digest} *${name}\n`); + writeFileSync(asset, "changed"); + expect(() => verifyChecksums(dir)).toThrow(/Checksum mismatch/); + rmSync(asset); + expect(() => verifyChecksums(dir)).toThrow(/which is missing/); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + test("rejects a tampered payload and a missing payload", () => { const dir = temporaryDirectory(); try { From 2f8216792faad8dbaf3d66700eaaf78867515479 Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 22 Sep 2026 23:19:43 +0900 Subject: [PATCH 16/23] fix(release): publish the GitHub release only after its verified assets attach (#5555) (#5558) GitHub freezes a release when it is published, so the attach step's upload came back HTTP 422 "Cannot upload assets to an immutable release". Every release from v2.55.0 to v2.60.0 therefore shipped with zero assets and the desktop updater had nothing to download. Create the release as a draft and flip it to published in attach-release, after the verified bundle is uploaded. Release notes still come from the validated notes file written at creation. --- .github/workflows/release.yml | 19 +++++++++++++++++-- tests/ci-workflows/ci-workflows.test.ts | 9 ++++++++- .../release-pipeline-contract.test.ts | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e62765c5358..4c06619bddd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -578,7 +578,19 @@ jobs: # run: source. tests/ci-workflows.test.ts enforces this repo-wide. RELEASE_VERSION: ${{ inputs.version }} run: | - gh release upload "v${RELEASE_VERSION}" dist/release/* --clobber + set -euo pipefail + release_tag="v${RELEASE_VERSION}" + gh release upload "$release_tag" dist/release/* --clobber + + # A published release is immutable: GitHub rejects every later asset upload + # with HTTP 422, which is why v2.55.0 through v2.60.0 shipped with zero + # assets and left the desktop updater without anything to download. The + # release is therefore created as a draft and becomes public here, once the + # verified bundle is attached. The only edit permitted is this flip — the + # notes still come from the validated notes file written at creation. + if [ "$(gh release view "$release_tag" --json isDraft --jq .isDraft)" = "true" ]; then + gh release edit "$release_tag" --draft=false + fi publish: needs: [validate-dispatch, verify-release] @@ -981,6 +993,9 @@ jobs: exit 1 fi else - gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \ + # Draft first. Publication freezes a release under GitHub's immutable + # releases, so attach-release attaches the verified bundle to the draft + # and publishes it afterwards. + gh release create "$release_tag" --draft --target "$GITHUB_SHA" --title "$release_tag" \ --notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag} fi diff --git a/tests/ci-workflows/ci-workflows.test.ts b/tests/ci-workflows/ci-workflows.test.ts index 6d3d2841baa..a7212745fbf 100644 --- a/tests/ci-workflows/ci-workflows.test.ts +++ b/tests/ci-workflows/ci-workflows.test.ts @@ -1033,7 +1033,14 @@ describe("GitHub Actions hardening", () => { expect(releaseNotesBuilder).toContain("release changelog failed coverage validation"); expect(workflow).toMatch(/gh release create[\s\S]*?--notes-file "\$notes_file"/); - expect(workflow).not.toContain("gh release edit"); + // The release is created as a draft and published only after the verified + // bundle is attached, because a published release is immutable and rejects + // every later upload. That draft flip is the one edit allowed: notes still + // come from the validated notes file, never from an edit or a regeneration. + for (const edit of workflow.match(/gh release edit[^\n]*/g) ?? []) { + expect(edit).toContain("--draft=false"); + expect(edit).not.toContain("--notes"); + } expect(workflow).not.toContain("--generate-notes"); const createStep = workflow diff --git a/tests/ci-workflows/release-pipeline-contract.test.ts b/tests/ci-workflows/release-pipeline-contract.test.ts index 8f474ae4b25..d2a90449fab 100644 --- a/tests/ci-workflows/release-pipeline-contract.test.ts +++ b/tests/ci-workflows/release-pipeline-contract.test.ts @@ -153,6 +153,22 @@ describe("release pipeline contract", () => { expect(upload).toBeGreaterThan(receiptCheck); }); + test("the release is a draft until the verified assets are attached", () => { + // GitHub freezes a release the moment it is published: every later asset upload + // comes back HTTP 422 "Cannot upload assets to an immutable release". Creating + // the release published and uploading afterwards is what left v2.55.0 through + // v2.60.0 with zero assets and the desktop updater with nothing to fetch. + const releaseText = readFileSync(repoPath(".github", "workflows", "release.yml"), "utf8"); + const createStep = releaseText.split("- name: Create GitHub release")[1] ?? ""; + expect(createStep).toMatch(/gh release create "\$release_tag" --draft/); + + const attachRun = (release.jobs?.["attach-release"]?.steps ?? []) + .map(candidate => candidate.run ?? "") + .find(run => run.includes("gh release upload")) ?? ""; + expect(attachRun).toContain("--draft=false"); + expect(attachRun.indexOf("gh release upload")).toBeLessThan(attachRun.indexOf("--draft=false")); + }); + test("a partial publication has a recorded, explicit recovery path", () => { const releaseText = readFileSync(repoPath(".github", "workflows", "release.yml"), "utf8"); // The only way npm publish is ever skipped: an explicit recovery input, requiring From 96b1406cb63e429cec8d2e3914af4ba99f2e37b9 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 23 Sep 2026 06:30:13 +0900 Subject: [PATCH 17/23] release: prepare 2.63.0 version metadata (#5612) --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index b2fbcb56130..33322e5d556 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2541,7 +2541,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.62.0" +version = "2.63.0" dependencies = [ "dbus", "reqwest 0.12.24", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 45b39cd890c..e46937659b0 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.62.0" +version = "2.63.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index ff6232c563c..36df79737fb 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.62.0", + "version": "2.63.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index 9c7252d8004..245a9b51a12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.62.0", + "version": "2.63.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From a32227bb979f6faf38cf592a0a702e099095cd79 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 07:24:26 +0900 Subject: [PATCH 18/23] release: prepare 2.69.0 version metadata --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 9bdc7f94b53..320a9a2b840 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2551,7 +2551,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.70.0" +version = "2.69.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index f2af7b8c59c..ed8f57bf1bb 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.70.0" +version = "2.69.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index da106098a8a..a3790fcbc8c 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.70.0", + "version": "2.69.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index fbe21ba9e8a..82f92b4f3cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.70.0", + "version": "2.69.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From 46c07c5e0f7157b19f79a06fbaf79d3e04c8308b Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 09:26:51 +0900 Subject: [PATCH 19/23] fix(release): sign the packaged macOS keyring addons before notarization (#6271) * fix(release): sign the packaged macOS keyring addons before notarization Notarization rejected the 2.73.0 preview app: Resources/keyring/*.node, bundled since #6161, were unsigned or ad-hoc and had no secure timestamp, and Tauri does not sign files under Resources. Sign each darwin addon in place with the Developer ID identity, hardened runtime and timestamp after the certificate import and before tauri build, verify the result, and fail a real release that lacks signing material. * fix(release): match keyring signature fields without a pipe (cherry picked from commit 11782eedc3758d90c037244ab1cf36f17a286ccc) --- .github/workflows/release.yml | 39 +++++++++++++++++++ .../release-desktop-scripts.test.ts | 17 ++++++++ 2 files changed, 56 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08a5e66cd05..764eb0622a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -413,6 +413,45 @@ jobs: echo "$description" | grep -q "flags=.*runtime" echo "$description" | grep -q "Timestamp=" + # Tauri signs the app, its sidecar and the widget it is handed, but nothing under Resources. + # The packaged keyring addons (#6161) are Mach-O code, so Apple notarization rejects the whole + # app unless each carries the Developer ID signature, the hardened runtime and a secure + # timestamp (2.73.0-preview.20260930 was refused for exactly that). Sign them in place, after + # the certificate import and before the bundler copies them. + - name: Sign the packaged keyring addons + if: runner.os == 'macOS' + env: + MACOS_SIGN_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + DRY_RUN: ${{ inputs.dry-run }} + run: | + set -euo pipefail + shopt -s nullglob + addons=(desktop/src-tauri/resources/keyring/*.darwin-*.node) + if [ "${#addons[@]}" -eq 0 ]; then + echo "::error::No macOS keyring addon was prepared for the bundle." + exit 1 + fi + if [ "${DESKTOP_SIGNING_CONFIGURED}" != "true" ]; then + if [ "${DRY_RUN}" != "true" ]; then + echo "::error::A real release must sign the packaged keyring addons." + exit 1 + fi + echo "No signing material; the keyring addons stay as prepared for this non-release build." + exit 0 + fi + for addon in "${addons[@]}"; do + codesign --force --timestamp --options runtime --sign "$MACOS_SIGN_IDENTITY" "$addon" + codesign --verify --strict "$addon" + description="$(codesign -dvvv "$addon" 2>&1)" + echo "$description" + # Here-strings, not pipes: under pipefail a matcher that exits on its first hit can + # SIGPIPE the writer and fail the assertion it was meant to pass. + grep -q "TeamIdentifier=$APPLE_TEAM_ID" <<<"$description" + grep -q "flags=.*runtime" <<<"$description" + grep -q "Timestamp=" <<<"$description" + done + - name: Prepare Windows installer version if: runner.os == 'Windows' shell: bash diff --git a/tests/ci-workflows/release-desktop-scripts.test.ts b/tests/ci-workflows/release-desktop-scripts.test.ts index db141b5e1c2..570e3067504 100644 --- a/tests/ci-workflows/release-desktop-scripts.test.ts +++ b/tests/ci-workflows/release-desktop-scripts.test.ts @@ -537,6 +537,23 @@ describe("widget extension signing", () => { .toContain("APPLE_CERTIFICATE"); }); + test("the packaged keyring addons are Developer ID signed before the bundler copies them", () => { + // Notarization refused 2.73.0-preview.20260930: Resources/keyring/*.node were ad-hoc or + // unsigned and had no secure timestamp, and Tauri does not sign files under Resources. + const sign = steps.find(step => step.name === "Sign the packaged keyring addons"); + expect(sign?.if).toBe("runner.os == 'macOS'"); + expect(sign?.env?.MACOS_SIGN_IDENTITY).toContain("APPLE_SIGNING_IDENTITY"); + expect(sign?.run).toContain("desktop/src-tauri/resources/keyring/*.darwin-*.node"); + expect(sign?.run).toContain('codesign --force --timestamp --options runtime --sign "$MACOS_SIGN_IDENTITY"'); + expect(sign?.run).toContain('grep -q "TeamIdentifier=$APPLE_TEAM_ID" <<<"$description"'); + expect(sign?.run).toContain('grep -q "Timestamp=" <<<"$description"'); + // A real release never falls back to unsigned addons; only a dry run may. + expect(sign?.run).toContain("A real release must sign the packaged keyring addons."); + expect(indexOfStepRunning("security create-keychain")).toBeLessThan(indexOfStep(sign!.name!)); + expect(indexOfStep("Prepare macOS sidecars")).toBeLessThan(indexOfStep(sign!.name!)); + expect(indexOfStep(sign!.name!)).toBeLessThan(indexOfStep("Build desktop bundles")); + }); + test("the certificate is importable before the widget is signed and is removed afterwards", () => { // codesign resolves an identity through the keychain search list, and Tauri does not build // its own keychain until the bundling step, which is after this one. From de5c672c5d283a681a95e84882a15c3461644f17 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 30 Sep 2026 20:48:56 +0900 Subject: [PATCH 20/23] chore(release): 2.74.0 --- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 23e7615e415..74ed32ff04c 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2645,7 +2645,7 @@ dependencies = [ [[package]] name = "opencodex-desktop" -version = "2.75.0" +version = "2.74.0" dependencies = [ "base64 0.22.1", "dbus", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 6aa1932c8f5..81e8e2b51da 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "opencodex-desktop" -version = "2.75.0" +version = "2.74.0" description = "OpenCodex desktop shell" authors = ["OpenCodex contributors"] license = "MIT" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 256d52efe12..87ae5a99695 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "OpenCodex", - "version": "2.75.0", + "version": "2.74.0", "identifier": "com.opencodex.desktop", "build": { "frontendDist": "../ui", diff --git a/package.json b/package.json index 138fd7eb09f..1808a150251 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.75.0", + "version": "2.74.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From c1dc8e4e1895276ed4f70240eebe2539fcfddd7c Mon Sep 17 00:00:00 2001 From: JUN Date: Tue, 6 Oct 2026 23:21:53 +0900 Subject: [PATCH 21/23] fix(deps): bump proxy-addr, fast-uri, ip-address and source-map-js for new advisories release.yml's dependency audit refused 2.79.0 on a critical advisory published after the candidate was cut: proxy-addr <2.0.8 is vulnerable to IP spoofing via IPv4-mapped IPv6 trust subnets (GHSA-jqcg-44mw-7w3h), reached through @modelcontextprotocol/sdk > express. gui/ reports source-map-js <1.2.2 (GHSA-68fv-2mgg-jv7q, vite > postcss). bun audit fix, lockfiles only, every bump inside its declared range: proxy-addr 2.0.7 -> 2.0.8, fast-uri 3.1.7 -> 3.1.8, ip-address 10.4.0 -> 10.7.1, and in gui/ source-map-js 1.2.1 -> 1.2.2. Both audits now report no vulnerabilities at --audit-level=high. --- bun.lock | 6 +++--- gui/bun.lock | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 4c123e4c3c3..10de7ca5cfc 100644 --- a/bun.lock +++ b/bun.lock @@ -191,7 +191,7 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], + "fast-uri": ["fast-uri@3.1.8", "", {}, "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], @@ -219,7 +219,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="], + "ip-address": ["ip-address@10.7.1", "", {}, "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -263,7 +263,7 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], diff --git a/gui/bun.lock b/gui/bun.lock index dc41b1bdb9c..2cbae98b06d 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -177,7 +177,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], From 0cd21698a86aee7c4830bdcac55e872af8440be4 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Wed, 7 Oct 2026 18:14:01 +0900 Subject: [PATCH 22/23] fix(deps): bump @modelcontextprotocol/sdk to 1.31.0 for GHSA-6qxp-vccf-f47h --- bun.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 10de7ca5cfc..46893143a37 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ "@hono/node-server": ["@hono/node-server@2.1.0", "", { "peerDependencies": { "hono": "^4" } }, "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.31.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-UvTMgnNlnIBO/22ob2RcVGDlcvOslQs8T59+FTGdA0L27a39fdGF/EDETNtDVK4DZGpwomlsYpRdA8UXcVL/pw=="], "@napi-rs/keyring": ["@napi-rs/keyring@1.3.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.3.0", "@napi-rs/keyring-darwin-x64": "1.3.0", "@napi-rs/keyring-freebsd-x64": "1.3.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", "@napi-rs/keyring-linux-arm64-musl": "1.3.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-musl": "1.3.0", "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", "@napi-rs/keyring-win32-x64-msvc": "1.3.0" } }, "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA=="], From b304f129d36a679c43d8228b82e45065013cd7b1 Mon Sep 17 00:00:00 2001 From: yobushka Date: Mon, 19 Oct 2026 16:34:09 +0300 Subject: [PATCH 23/23] fix(oauth): allow cancelLoginFlow to dismiss settled terminal error states (#7378) --- src/oauth/login-flow-state.ts | 6 +++- ...auth-cancel-clears-transient-error.test.ts | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/oauth/oauth-cancel-clears-transient-error.test.ts diff --git a/src/oauth/login-flow-state.ts b/src/oauth/login-flow-state.ts index 3a52a6a7151..0de081f63e1 100644 --- a/src/oauth/login-flow-state.ts +++ b/src/oauth/login-flow-state.ts @@ -305,7 +305,11 @@ export function cancelLoginFlow(provider: string, flowId?: string): boolean { const active = loginAbort.get(flowKey); const existing = loginState.get(flowKey); if ((active?.provider ?? loginFlowProvider(flowKey)) !== provider) return false; - if (!active && (!existing || existing.done)) return false; + if (!active) { + if (!existing) return false; + dropLoginFlow(flowKey); + return true; + } abandonLoginFlow(flowKey, { error: "Login cancelled", errorCode: "no_pending_login" }); return true; } diff --git a/tests/oauth/oauth-cancel-clears-transient-error.test.ts b/tests/oauth/oauth-cancel-clears-transient-error.test.ts new file mode 100644 index 00000000000..a3306d777cf --- /dev/null +++ b/tests/oauth/oauth-cancel-clears-transient-error.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, test } from "bun:test"; +import { cancelLoginFlow, getLoginStatus } from "../../src/oauth"; +import { loginState } from "../../src/oauth/login-flow-state"; + +describe("OAuth login cancellation and error lifecycle", () => { + test("cancelLoginFlow clears finalized transient error state so it stops masking status", () => { + const provider = "google-antigravity"; + // Simulate a failed/aborted login attempt that settled with an error + loginState.set(provider, { + done: true, + error: "OAuth authentication failed. Check the OpenCodex account status and retry.", + }); + + // Before cancel, getLoginStatus reflects the error + let status = getLoginStatus(provider); + expect(status.error).toBe("OAuth authentication failed. Check the OpenCodex account status and retry."); + expect(status.done).toBe(true); + + // Call cancelLoginFlow + const cancelled = cancelLoginFlow(provider); + expect(cancelled).toBe(true); + + // After cancel, the transient error must be cleared + status = getLoginStatus(provider); + expect(status.error).toBeUndefined(); + expect(status.done).toBe(false); + }); +});