From 71c57ea647fbc376d1207f11d851c09504c9c02d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 10:37:05 +0900 Subject: [PATCH 1/4] release: v2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f73ed2d0e5..063ecfe73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.0", + "version": "2.32.1", "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 ec51e42d745d2645bcb22cb67855fa053ba1778e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 20:25:22 +0900 Subject: [PATCH 2/4] release: v2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 063ecfe73e..6f8499ffbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.1", + "version": "2.33.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 aaa9eaf37058965373dc42d1ca344e987950b6b6 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 2 Sep 2026 18:43:29 +0900 Subject: [PATCH 3/4] 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 458bb67e0a..261aece1d1 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 1ea3f4795df2f54d8c4c95919928cdba509beda5 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 14:46:16 +0900 Subject: [PATCH 4/4] fix(client): clear disconnected hub token on recycle --- src/client/machine-api.ts | 4 ++-- src/client/machine-listener.ts | 4 ++-- src/client/runtime.ts | 18 +++++++++++++++-- tests/client-machine-listener.test.ts | 4 +++- tests/client-runtime.test.ts | 29 +++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 tests/client-runtime.test.ts diff --git a/src/client/machine-api.ts b/src/client/machine-api.ts index fe92a4a88c..b27506daeb 100644 --- a/src/client/machine-api.ts +++ b/src/client/machine-api.ts @@ -23,7 +23,7 @@ export interface MachineStatusV1 { export interface MachineApiDeps { sync: typeof syncConnectedClient; disconnect: typeof disconnectClient; - scheduleStandaloneRecycle: () => void; + scheduleStandaloneRecycle: (disconnectedTokenFingerprint: string) => void; hubReachability?: () => HubReachability; setHubReachability?: (value: HubReachability) => void; } @@ -129,7 +129,7 @@ export async function handleMachineApi( } try { const result = await deps.disconnect(input.keepCatalog === undefined ? {} : { keepCatalog: input.keepCatalog }); - deps.scheduleStandaloneRecycle(); + deps.scheduleStandaloneRecycle(state.tokenFingerprint); return Response.json({ success: true, ...result }, { status: 202 }); } catch (error) { return Response.json({ success: false, error: error instanceof Error ? error.message : "disconnect failed" }, { status: 409 }); diff --git a/src/client/machine-listener.ts b/src/client/machine-listener.ts index b7e54032b6..418c035182 100644 --- a/src/client/machine-listener.ts +++ b/src/client/machine-listener.ts @@ -70,8 +70,8 @@ export function startMachineListener( const machineApiDeps: MachineApiDeps = { sync: deps.machineApi?.sync ?? syncConnectedClient, disconnect: deps.machineApi?.disconnect ?? disconnectClient, - scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (() => { - void import("./runtime").then(module => module.scheduleStandaloneRecycle()); + scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (tokenFingerprint => { + void import("./runtime").then(module => module.scheduleStandaloneRecycle(tokenFingerprint)); }), hubReachability: deps.machineApi?.hubReachability ?? (() => hubReachability), setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }), diff --git a/src/client/runtime.ts b/src/client/runtime.ts index f8eb85920a..cff840ca41 100644 --- a/src/client/runtime.ts +++ b/src/client/runtime.ts @@ -4,6 +4,7 @@ import { loadConfig } from "../config"; import { removePid, removeRuntimePort, writePid, writeRuntimePort } from "../config/process-state"; import { installCrashGuards } from "../lib/crash-guard"; import { selfLaunchArgv } from "../lib/self-launch-argv"; +import { serviceApiTokenFingerprint } from "../lib/service-secrets"; import { findAvailablePort } from "../server/ports"; import { startMachineListener } from "./machine-listener"; import { readClientConnectionState } from "./state"; @@ -17,7 +18,20 @@ function cleanup(): void { removeRuntimePort(process.pid); } -export function scheduleStandaloneRecycle(): void { +export function standaloneRecycleEnv( + env: NodeJS.ProcessEnv, + disconnectedTokenFingerprint: string, +): NodeJS.ProcessEnv { + const childEnv = { ...env }; + const admissionToken = childEnv.OPENCODEX_API_AUTH_TOKEN?.trim(); + if (admissionToken && serviceApiTokenFingerprint(admissionToken) === disconnectedTokenFingerprint) { + delete childEnv.OPENCODEX_API_AUTH_TOKEN; + delete childEnv.OCX_API_TOKEN_FILE; + } + return childEnv; +} + +export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string): void { if (recycleScheduled) return; recycleScheduled = true; const timer = setTimeout(() => { @@ -46,7 +60,7 @@ export function scheduleStandaloneRecycle(): void { detached: true, stdio: "ignore", windowsHide: true, - env: { ...process.env }, + env: standaloneRecycleEnv(process.env, disconnectedTokenFingerprint), }); child.unref(); } diff --git a/tests/client-machine-listener.test.ts b/tests/client-machine-listener.test.ts index b4838718fe..c658b426e9 100644 --- a/tests/client-machine-listener.test.ts +++ b/tests/client-machine-listener.test.ts @@ -139,7 +139,9 @@ describe("client machine listener", () => { disconnected = true; return { restored: true, tokenRemoved: true, catalogRemoved: true, apiKeyId: "client-key-a" }; }, - scheduleStandaloneRecycle: () => { recycled = disconnected; }, + scheduleStandaloneRecycle: tokenFingerprint => { + recycled = disconnected && tokenFingerprint === connection().tokenFingerprint; + }, }, }); servers.push(server); diff --git a/tests/client-runtime.test.ts b/tests/client-runtime.test.ts new file mode 100644 index 0000000000..81bbf25324 --- /dev/null +++ b/tests/client-runtime.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from "bun:test"; +import { serviceApiTokenFingerprint } from "../src/lib/service-secrets"; +import { standaloneRecycleEnv } from "../src/client/runtime"; + +describe("standalone recycle environment", () => { + test("removes a disconnected hub token and its token-file source", () => { + const hubToken = "hub-issued-token"; + const source = { + OPENCODEX_API_AUTH_TOKEN: hubToken, + OCX_API_TOKEN_FILE: "/tmp/hub-service-token", + PATH: "/usr/bin", + }; + + expect(standaloneRecycleEnv(source, serviceApiTokenFingerprint(hubToken))).toEqual({ + PATH: "/usr/bin", + }); + expect(source.OPENCODEX_API_AUTH_TOKEN).toBe(hubToken); + }); + + test("preserves an independently configured operator credential", () => { + const operatorToken = "operator-token"; + const source = { + OPENCODEX_API_AUTH_TOKEN: operatorToken, + OCX_API_TOKEN_FILE: "/tmp/operator-token", + }; + + expect(standaloneRecycleEnv(source, serviceApiTokenFingerprint("disconnected-hub-token"))).toEqual(source); + }); +});