fix(core): keep last known remote config when refresh fails (#53667)

Fixes #53666

Co-authored-by: Ayush Thakur <51413362+Ayushlm10@users.noreply.github.com>
This commit is contained in:
Ayush Thakur
2026-10-08 09:26:24 +05:30
committed by GitHub
Unverified
parent a80ca68c96
commit f108fa4928
4 changed files with 188 additions and 16 deletions
+11 -11
View File
@@ -144,19 +144,19 @@ export const layer = (options?: Options) =>
const loadWellknownEntry = Effect.fnUntraced(function* (entry: WellKnown.Entry) {
const auth = entry.manifest.auth
if (!auth) return []
const credential = (yield* credentials.list(entry.integrationID)).at(-1)
if (!credential || credential.value.type !== "key") return []
const variables = { [auth.env]: credential.value.key }
const configs = yield* wellknown
.resolve(entry, variables)
.pipe(
Effect.catch(() =>
Effect.logWarning("failed to load wellknown config", { source: entry.origin }).pipe(
Effect.as([] as const),
),
const variables = auth && credential?.value.type === "key" ? { [auth.env]: credential.value.key } : undefined
// A failed refresh, including a missing credential, keeps the last resolved config so remote providers
// and allowlists do not disappear; only removing the source clears it.
const configs = yield* (
variables ? wellknown.resolve(entry, variables) : Effect.fail(new Error("No usable wellknown credential"))
).pipe(
Effect.catch((error) =>
Effect.logWarning("failed to load wellknown config", { source: entry.origin, error }).pipe(
Effect.andThen(wellknown.cached(entry.origin)),
),
)
),
)
return yield* Effect.forEach(configs, (config) =>
ConfigVariable.substitute({
type: "virtual",
+33 -4
View File
@@ -48,7 +48,10 @@ export interface Interface {
readonly refresh: () => Effect.Effect<boolean, Error>
readonly add: (origin: string) => Effect.Effect<Entry, Error>
readonly remove: (origin: string) => Effect.Effect<void>
/** Resolves and caches the configuration for a registered source. */
readonly resolve: (entry: Entry, variables: Readonly<Record<string, string>>) => Effect.Effect<Config[], Error>
/** Returns the last configuration resolved for an origin, kept until the source is removed. */
readonly cached: (origin: string) => Effect.Effect<Config[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/WellKnown") {}
@@ -97,6 +100,9 @@ const resolveEntry = Effect.fnUntraced(function* (entry: Entry, variables: Reado
const sourcesKey = "wellknown:sources"
const Sources = Schema.Array(Schema.String)
const manifestKey = (origin: string) => `wellknown:manifest:${origin}`
const configKey = (origin: string) => `wellknown:config:${origin}`
const Configs = Schema.Array(Config)
const layer = Layer.effect(
Service,
@@ -107,7 +113,22 @@ const layer = Layer.effect(
const cache = yield* Ref.make(new Map<string, Entry>())
const lock = Semaphore.makeUnsafe(1)
const loadEntry = Effect.fn("WellKnown.loadEntry")(function* (origin: string) {
const manifest = yield* inspect(origin).pipe(Effect.provideService(HttpClient.HttpClient, http))
const manifest = yield* inspect(origin).pipe(
Effect.provideService(HttpClient.HttpClient, http),
Effect.tap((manifest) => kv.set(manifestKey(origin), manifest)),
// An unreachable source keeps its last manifest so its remote config still loads after a restart.
Effect.catch((error) =>
kv
.get(manifestKey(origin))
.pipe(
Effect.flatMap((cached) =>
Schema.is(Manifest)(cached)
? Effect.logWarning("failed to load wellknown manifest", { origin, error }).pipe(Effect.as(cached))
: Effect.fail(error),
),
),
),
)
return { origin, integrationID: Integration.ID.make(origin), manifest }
})
@@ -167,6 +188,8 @@ const layer = Layer.effect(
sourcesKey,
origins.filter((item) => item !== origin),
)
yield* kv.remove(manifestKey(origin))
yield* kv.remove(configKey(origin))
yield* Ref.update(cache, (current) => {
const next = new Map(current)
next.delete(origin)
@@ -176,9 +199,15 @@ const layer = Layer.effect(
},
(effect, _value) => lock.withPermit(effect),
),
resolve: Effect.fn("WellKnown.resolveEntry")((entry, variables) =>
resolveEntry(entry, variables).pipe(Effect.provideService(HttpClient.HttpClient, http)),
),
resolve: Effect.fn("WellKnown.resolveEntry")(function* (entry, variables) {
const configs = yield* resolveEntry(entry, variables).pipe(Effect.provideService(HttpClient.HttpClient, http))
yield* kv.set(configKey(entry.origin), configs)
return configs
}),
cached: Effect.fn("WellKnown.cached")(function* (origin) {
const value = yield* kv.get(configKey(origin))
return Schema.is(Configs)(value) ? [...value] : []
}),
})
}),
)
+143 -1
View File
@@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test"
import { Effect, Fiber, Layer, Logger, Schema, Stream } from "effect"
import { FastCheck } from "effect/testing"
import { Config } from "@opencode/core/config"
import { Directory, Document, Event, Info } from "@opencode/schema/config"
import { Directory, Document, type Entry, Event, Info } from "@opencode/schema/config"
import { ConfigModel } from "@opencode/schema/config/model"
import { ConfigProvider } from "@opencode/schema/config/provider"
import { AppNodeBuilder } from "@opencode/core/effect/app-node-builder"
@@ -21,6 +21,7 @@ import { Project } from "@opencode/core/project"
import { Provider } from "@opencode/core/provider"
import { AbsolutePath } from "@opencode/core/schema"
import { WellKnown } from "@opencode/core/wellknown"
import { KV } from "@opencode/core/kv"
import { Integration } from "@opencode/schema/integration"
import { emptyCredentialNode, emptyWellknownNode } from "../fixture/config-nodes"
import { location } from "../fixture/location"
@@ -512,6 +513,7 @@ describe("Config", () => {
available
? Effect.succeed([{ shell: variables.TOKEN }])
: Effect.fail(new Error("expired credential")),
cached: () => Effect.succeed([]),
}),
),
deps: [],
@@ -550,6 +552,145 @@ describe("Config", () => {
),
)
it.live("keeps the last resolved wellknown config across failures and restarts until its source is removed", () =>
Effect.acquireUseRelease(
Effect.promise(() => tmpdir()),
(tmp) => {
let manifestStatus = 200
let configStatus = 200
let requests = 0
let key: string | undefined = "secret"
const server = Bun.serve({
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/.well-known/opencode") {
if (manifestStatus !== 200) return new Response("Unavailable", { status: manifestStatus })
return Response.json({
auth: { command: ["login"], env: "TOKEN" },
remote_config: { url: `${url.origin}/config`, headers: { authorization: "Bearer {env:TOKEN}" } },
})
}
requests++
if (configStatus !== 200) return new Response("Unavailable", { status: configStatus })
return Response.json({
config: {
enabled_providers: ["remote"],
provider: { remote: { npm: "@ai-sdk/openai-compatible", models: { chat: {} } } },
},
})
},
})
const origin = server.url.origin
const integrationID = Integration.ID.make(origin)
const credentialNode = makeGlobalNode({
service: Credential.Service,
layer: Layer.mock(Credential.Service)({
list: () =>
Effect.succeed(
key === undefined
? []
: [
new Credential.Info({
id: Credential.ID.create(),
integrationID,
label: "default",
value: Credential.Key.make({ type: "key", key }),
}),
],
),
}),
deps: [],
})
const global = path.join(tmp.path, "global")
const project = path.join(tmp.path, "project")
const remote = (entries: readonly Entry[]) => ({
provider: Config.latest(entries, "providers")?.remote?.package,
policies: Config.latest(entries, "experimental")?.policies,
})
const loaded = {
provider: "aisdk:@ai-sdk/openai-compatible",
policies: [
{ action: "provider.use", resource: "*", effect: "deny" },
{ action: "provider.use", resource: "remote", effect: "allow" },
],
} as const
const removed = { provider: undefined, policies: undefined }
return Effect.gen(function* () {
yield* Effect.promise(() => fs.mkdir(project, { recursive: true }))
const kv = Layer.succeedContext(yield* Layer.build(LayerNode.compile(KV.node)))
// Each build is a fresh process sharing one persisted KV store.
const instance = () =>
AppNodeBuilder.build(LayerNode.group([Config.node, Bus.node, WellKnown.node]), [
Location.node.replace(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(project) })),
),
),
Global.node.replace(Global.layerWith({ config: global, home: path.join(global, "home") })),
Credential.node.replace(credentialNode),
KV.node.replace(kv),
Watcher.node.replace(Watcher.testLayer),
])
const services = Effect.gen(function* () {
const config = yield* Config.Service
const bus = yield* Bus.Service
// Each reload also rewrites a local file so Config publishes an update to wait on.
const reload = Effect.fnUntraced(function* (shell: string, trigger: Effect.Effect<unknown, Error>) {
const updated = yield* bus
.subscribe(Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
yield* Effect.promise(() => fs.writeFile(path.join(project, "opencode.json"), JSON.stringify({ shell })))
yield* trigger
yield* Fiber.join(updated)
return remote(yield* config.entries())
})
const switched = bus.publish(
Credential.Event.Switched,
{ credentialID: Credential.ID.create(), integrationID },
{ global: true },
)
return { config, bus, wellknown: yield* WellKnown.Service, reload, switched }
})
yield* Effect.gen(function* () {
const warm = yield* services
expect(yield* warm.reload("initial", warm.wellknown.add(origin))).toEqual(loaded)
configStatus = 503
const before = requests
expect(yield* warm.reload("unavailable", warm.switched)).toEqual(loaded)
expect(requests).toBeGreaterThan(before)
key = undefined
expect(yield* warm.reload("logged-out", warm.switched)).toEqual(loaded)
manifestStatus = 503
const updated = yield* warm.bus
.subscribe(WellKnown.Event.Updated)
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped({ startImmediately: true }))
expect(yield* warm.wellknown.refresh()).toBe(false)
yield* Effect.sleep("50 millis")
expect(updated.pollUnsafe()).toBeUndefined()
}).pipe(Effect.scoped, Effect.provide(instance()))
key = "secret"
yield* Effect.gen(function* () {
const cold = yield* services
expect(remote(yield* cold.config.entries())).toEqual(loaded)
// The wellknown plugin loads these entries at startup and dies if they fail.
expect(yield* cold.wellknown.entries()).toHaveLength(1)
expect(yield* cold.reload("removed", cold.wellknown.remove(origin))).toEqual(removed)
manifestStatus = 200
expect(yield* cold.reload("re-added", cold.wellknown.add(origin))).toEqual(removed)
}).pipe(Effect.scoped, Effect.provide(instance()))
}).pipe(Effect.ensuring(Effect.promise(() => server.stop(true))))
},
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
),
)
it.live("logs redacted source-aware diagnostics for every config source", () => {
const output: Array<Record<string, unknown>> = []
const logger = Logger.map(Logger.formatStructured, (entry) => {
@@ -613,6 +754,7 @@ describe("Config", () => {
remove: () => Effect.die("unused Wellknown.remove"),
// Exercise the loader boundary against a malformed implementation response.
resolve: () => Effect.succeed([null as unknown as WellKnown.Config]),
cached: () => Effect.succeed([]),
}),
),
deps: [],
@@ -31,6 +31,7 @@ export const emptyWellknownNode = makeGlobalNode({
add: () => Effect.die("unused Wellknown.add"),
remove: () => Effect.die("unused Wellknown.remove"),
resolve: () => Effect.die("unused Wellknown.resolve"),
cached: () => Effect.succeed([]),
}),
),
deps: [],