From 613d93c49920a17ce7fd5fb406150552d7825b05 Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 00:46:25 +0530 Subject: [PATCH 1/2] fix(opencode): serialize concurrent auth.json writes Ported from upstream anomalyco/opencode#36362. --- packages/opencode/src/auth/index.ts | 37 +++++++++++++++--------- packages/opencode/test/auth/auth.test.ts | 17 +++++++++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index a133e88498d5..c90589997376 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,6 +1,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" import path from "path" -import { Effect, Layer, Record, Result, Schema, Context } from "effect" +import { Effect, Layer, Record, Result, Schema, Context, Semaphore } from "effect" import { NonNegativeInt } from "@opencode-ai/core/schema" import { Global } from "@opencode-ai/core/global" import { FSUtil } from "@opencode-ai/core/fs-util" @@ -54,6 +54,9 @@ const layer = Layer.effect( Effect.gen(function* () { const fsys = yield* FSUtil.Service const decode = Schema.decodeUnknownOption(Info) + // Serialize auth.json mutations so concurrent set/remove cannot clobber each other + // (unlocked read-modify-write previously dropped providers under parallel login). + const lock = Semaphore.makeUnsafe(1) const all = Effect.fn("Auth.all")(function* () { if (process.env.OPENCODE_AUTH_CONTENT) { @@ -71,21 +74,29 @@ const layer = Layer.effect( }) const set = Effect.fn("Auth.set")(function* (key: string, info: Info) { - const norm = key.replace(/\/+$/, "") - const data = yield* all() - if (norm !== key) delete data[key] - delete data[norm + "/"] - yield* fsys - .writeJson(file, { ...data, [norm]: info }, 0o600) - .pipe(Effect.mapError(fail("Failed to write auth data"))) + yield* lock.withPermits(1)( + Effect.gen(function* () { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + if (norm !== key) delete data[key] + delete data[norm + "/"] + yield* fsys + .writeJson(file, { ...data, [norm]: info }, 0o600) + .pipe(Effect.mapError(fail("Failed to write auth data"))) + }), + ) }) const remove = Effect.fn("Auth.remove")(function* (key: string) { - const norm = key.replace(/\/+$/, "") - const data = yield* all() - delete data[key] - delete data[norm] - yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) + yield* lock.withPermits(1)( + Effect.gen(function* () { + const norm = key.replace(/\/+$/, "") + const data = yield* all() + delete data[key] + delete data[norm] + yield* fsys.writeJson(file, data, 0o600).pipe(Effect.mapError(fail("Failed to write auth data"))) + }), + ) }) return Service.of({ get, all, set, remove }) diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts index bb72be66e57c..01fa6a28f062 100644 --- a/packages/opencode/test/auth/auth.test.ts +++ b/packages/opencode/test/auth/auth.test.ts @@ -72,4 +72,21 @@ describe("Auth", () => { expect(after["anthropic"]).toBeUndefined() }), ) + + // #30396 — concurrent Auth.set must not clobber sibling providers + it.instance("set is safe under concurrent calls", () => + Effect.gen(function* () { + const auth = yield* Auth.Service + yield* Effect.all( + [ + auth.set("anthropic", { type: "api", key: "k1" }), + auth.set("openai", { type: "api", key: "k2" }), + auth.set("google", { type: "api", key: "k3" }), + ], + { concurrency: "unbounded" }, + ) + const data = yield* auth.all() + expect(Object.keys(data).sort()).toEqual(["anthropic", "google", "openai"]) + }), + ) }) From 8a3ba4bc98fc0ae44fa7789b068320eb57e495c0 Mon Sep 17 00:00:00 2001 From: CreatorGhost Date: Sun, 12 Jul 2026 06:46:31 +0530 Subject: [PATCH 2/2] test(auth): cover concurrent set/remove interaction Addresses CodeRabbit review finding on the #36362 port. --- packages/opencode/test/auth/auth.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts index 01fa6a28f062..132d3106f472 100644 --- a/packages/opencode/test/auth/auth.test.ts +++ b/packages/opencode/test/auth/auth.test.ts @@ -89,4 +89,19 @@ describe("Auth", () => { expect(Object.keys(data).sort()).toEqual(["anthropic", "google", "openai"]) }), ) + + it.instance("set and remove are safe under concurrent calls", () => + Effect.gen(function* () { + const auth = yield* Auth.Service + yield* auth.set("anthropic", { type: "api", key: "k1" }) + + yield* Effect.all([auth.set("openai", { type: "api", key: "k2" }), auth.remove("anthropic")], { + concurrency: "unbounded", + }) + + const data = yield* auth.all() + expect(data["openai"]).toBeDefined() + expect(data["anthropic"]).toBeUndefined() + }), + ) })