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 88613a259d2fb6209b59546cce7fd811ec02ffdc Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 14:50:23 +0900 Subject: [PATCH 4/4] fix(client): block loopback session mutations --- src/client/machine-listener.ts | 8 ++++++++ tests/client-machine-listener.test.ts | 19 ++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/client/machine-listener.ts b/src/client/machine-listener.ts index b7e54032b6..e23519b083 100644 --- a/src/client/machine-listener.ts +++ b/src/client/machine-listener.ts @@ -111,6 +111,14 @@ export function startMachineListener( if (managementPrincipal(req, managementAuth, config) !== "gui-session") { return Response.json({ error: "opencodex machine GUI session required" }, { status: 401 }); } + // A loopback dashboard session proves possession, not user presence: any local + // process can fetch the dashboard bootstrap and replay its token and CSRF value. + // Keep the connected listener useful for status/diagnostics, but never let that + // credentialless bootstrap authorize durable machine changes. Those operations + // remain available through the explicit CLI commands. + if (req.method !== "GET" && req.method !== "HEAD") { + return Response.json({ error: "opencodex machine changes require the local CLI" }, { status: 403 }); + } return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req); } diff --git a/tests/client-machine-listener.test.ts b/tests/client-machine-listener.test.ts index b4838718fe..fef193fc1b 100644 --- a/tests/client-machine-listener.test.ts +++ b/tests/client-machine-listener.test.ts @@ -97,7 +97,7 @@ describe("client machine listener", () => { expect((await fetch(new URL("/api/machine/status", server.url), { method: "POST" })).status).toBe(404); }); - test("requires a GUI session for safe reads and Origin plus CSRF for mutations", async () => { + test("allows GUI-session reads but refuses mutations from a credentialless bootstrap", async () => { let syncCalls = 0; const server = startMachineListener(0, { state: connection(), @@ -124,11 +124,16 @@ describe("client machine listener", () => { expect((await fetch(syncUrl, { method: "POST", headers: safeHeaders, body: "{}" })).status).toBe(401); expect(syncCalls).toBe(0); const mutationHeaders = await guiHeaders(server, true); - expect((await fetch(syncUrl, { method: "POST", headers: mutationHeaders, body: "{}" })).status).toBe(200); - expect(syncCalls).toBe(1); + expect((await fetch(syncUrl, { method: "POST", headers: mutationHeaders, body: "{}" })).status).toBe(403); + expect((await fetch(new URL("/api/machine/shim", server.url), { + method: "POST", + headers: mutationHeaders, + body: JSON.stringify({ action: "uninstall" }), + })).status).toBe(403); + expect(syncCalls).toBe(0); }); - test("disconnect commits before 202 and schedules standalone recycle while the hub is offline", async () => { + test("does not let a bootstrapped GUI session disconnect or recycle the machine", async () => { let disconnected = false; let recycled = false; const server = startMachineListener(0, { @@ -148,9 +153,9 @@ describe("client machine listener", () => { headers: await guiHeaders(server, true), body: "{}", }); - expect(response.status).toBe(202); - expect(disconnected).toBe(true); - expect(recycled).toBe(true); + expect(response.status).toBe(403); + expect(disconnected).toBe(false); + expect(recycled).toBe(false); }); test("refuses startup without matching durable connected state", () => {