Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d35592b
merge dev into main for the v2.32.1 release
lidge-jun Aug 25, 2026
71c57ea
release: v2.32.1
lidge-jun Aug 25, 2026
d560ac6
merge dev into main for the v2.33.0 release
lidge-jun Aug 25, 2026
08ada6f
Merge pull request #2553 from lidge-jun/codex/promote-main-2330
lidge-jun Aug 25, 2026
ec51e42
release: v2.33.0
lidge-jun Aug 25, 2026
e25b653
merge dev into main for the v2.34.0 release
lidge-jun Aug 27, 2026
80fff9a
Merge pull request #2760 from lidge-jun/codex/promote-main-2340
lidge-jun Aug 27, 2026
fc4de77
Merge pull request #2826 from lidge-jun/codex/promote-main-2350
lidge-jun Aug 28, 2026
c7d8407
Merge pull request #3002 from lidge-jun/codex/promote-main-2360
lidge-jun Aug 30, 2026
54e2274
Merge pull request #3037 from lidge-jun/codex/promote-main-2370
lidge-jun Aug 31, 2026
2c4dca1
merge dev into the promotion branch for v2.38.0
lidge-jun Aug 31, 2026
a34e8b7
merge dev into the promotion branch for v2.38.0 (picks up the ReDoS fix)
lidge-jun Aug 31, 2026
ebb4d55
Merge pull request #3073 from lidge-jun/codex/promote-main-2380
lidge-jun Aug 31, 2026
682112e
Merge remote-tracking branch 'origin/dev' into codex/promote-main-2390
lidge-jun Sep 1, 2026
af6113a
merge dev into main for the v2.39.0 release
lidge-jun Sep 1, 2026
847f4f1
merge dev into main for the v2.40.0 release
Sep 2, 2026
ac78647
Merge pull request #3261 from lidge-jun/codex/promote-main-2400
lidge-jun Sep 2, 2026
aaa9eaf
fix(release): pass the bump job's permissions through the reusable-wo…
lidge-jun Sep 2, 2026
35ff3a4
Merge pull request #3263 from lidge-jun/codex/promote-main-2400-relfix
lidge-jun Sep 2, 2026
88613a2
fix(client): block loopback session mutations
luvs01 Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
8 changes: 8 additions & 0 deletions src/client/machine-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Comment on lines +119 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Retire dashboard controls that this guard makes unusable

When the dashboard is served by a connected client, it still exposes actions that POST to these machine endpoints: disconnect in gui/src/App.tsx:221-227, sync in gui/src/pages/Integrations.tsx:72-80, and shim repair/removal in gui/src/pages/Startup.tsx:105-117. This unconditional method guard makes every one of those controls return 403 for every connected-dashboard user; sync and shim actions silently do nothing, while disconnect reports an error. Remove or disable the controls for connected clients and direct users to the corresponding CLI commands, with the user-facing behavior documented.

AGENTS.md reference: AGENTS.md:L343-L344

Useful? React with 👍 / 👎.

}
return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req);
}

Expand Down
19 changes: 12 additions & 7 deletions tests/client-machine-listener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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, {
Expand All @@ -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", () => {
Expand Down
Loading