From d9c18381a67da445189d74e230728e737ef161c5 Mon Sep 17 00:00:00 2001 From: Dax Date: Wed, 6 May 2026 11:12:23 -0400 Subject: [PATCH] feat(config): support well-known remote_config (#26054) --- packages/opencode/src/config/config.ts | 57 ++++++++++++++- packages/opencode/test/config/config.test.ts | 77 ++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3a933f81e9..6b43b18968 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -70,6 +70,40 @@ function normalizeLoadedConfig(data: unknown, source: string) { return copy } +async function substituteWellKnownRemoteConfig(input: { + value: unknown + dir: string + source: string +}) { + if (!isRecord(input.value) || typeof input.value.url !== "string") return + + const url = await ConfigVariable.substitute({ + text: input.value.url, + type: "virtual", + dir: input.dir, + source: input.source, + }) + const headers = isRecord(input.value.headers) + ? Object.fromEntries( + await Promise.all( + Object.entries(input.value.headers) + .filter((entry): entry is [string, string] => typeof entry[1] === "string") + .map(async ([key, value]) => [ + key, + await ConfigVariable.substitute({ + text: value, + type: "virtual", + dir: input.dir, + source: input.source, + }), + ]), + ), + ) + : undefined + + return { url, headers } +} + async function resolveLoadedPlugins(config: T, filepath: string) { if (!config.plugin) return config for (let i = 0; i < config.plugin.length; i++) { @@ -494,8 +528,27 @@ export const layer = Layer.effect( if (!response.ok) { throw new Error(`failed to fetch remote config from ${url}: ${response.status}`) } - const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record } - const remoteConfig = wellknown.config ?? {} + const wellknown = (yield* Effect.promise(() => response.json())) as { + config?: Record + remote_config?: unknown + } + const remote = yield* Effect.promise(() => + substituteWellKnownRemoteConfig({ + value: wellknown.remote_config, + dir: url, + source: `${url}/.well-known/opencode`, + }), + ) + const fetchedConfig = remote + ? ((yield* Effect.promise(async () => { + log.debug("fetching remote config", { url: remote.url }) + const response = await fetch(remote.url, { headers: remote.headers }) + if (!response.ok) throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`) + const data = await response.json() + return isRecord(data) && isRecord(data.config) ? data.config : data + })) as Record) + : {} + const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info) if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" const source = `${url}/.well-known/opencode` const next = yield* loadConfig(JSON.stringify(remoteConfig), { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 0a522b0850..bbe585237b 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1972,6 +1972,83 @@ test("wellknown URL with trailing slash is normalized", async () => { } }) +test("wellknown remote_config supports templated env vars in headers", async () => { + const originalFetch = globalThis.fetch + const originalToken = process.env.TEST_TOKEN + let wellknownFetchedUrl: string | undefined + let remoteFetchedUrl: string | undefined + let remoteHeaders: HeadersInit | undefined + globalThis.fetch = mock((url: string | URL | Request, init?: RequestInit) => { + const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url + if (urlStr.includes(".well-known/opencode")) { + wellknownFetchedUrl = urlStr + return Promise.resolve( + new Response( + JSON.stringify({ + remote_config: { + url: "https://config.example.com/opencode.json", + headers: { + Authorization: "Bearer {env:TEST_TOKEN}", + }, + }, + }), + { status: 200 }, + ), + ) + } + if (urlStr.includes("config.example.com")) { + remoteFetchedUrl = urlStr + remoteHeaders = init?.headers + return Promise.resolve( + new Response( + JSON.stringify({ + mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } }, + }), + { status: 200 }, + ), + ) + } + return originalFetch(url, init) + }) as unknown as typeof fetch + + const fakeAuth = Layer.mock(Auth.Service)({ + all: () => + Effect.succeed({ + "https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }), + }), + }) + + const layer = Config.layer.pipe( + Layer.provide(testFlock), + Layer.provide(AppFileSystem.defaultLayer), + Layer.provide(Env.defaultLayer), + Layer.provide(fakeAuth), + Layer.provide(emptyAccount), + Layer.provideMerge(infra), + Layer.provide(noopNpm), + ) + + try { + await provideTmpdirInstance( + () => + Config.Service.use((svc) => + Effect.gen(function* () { + const config = yield* svc.get() + expect(wellknownFetchedUrl).toBe("https://example.com/.well-known/opencode") + expect(remoteFetchedUrl).toBe("https://config.example.com/opencode.json") + expect(remoteHeaders).toEqual({ Authorization: "Bearer test-token" }) + expect(config.mcp?.confluence?.enabled).toBe(true) + }), + ), + { git: true }, + ).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise) + } finally { + globalThis.fetch = originalFetch + if (originalToken === undefined) delete process.env.TEST_TOKEN + else process.env.TEST_TOKEN = originalToken + } +}) + describe("resolvePluginSpec", () => { test("keeps package specs unchanged", async () => { await using tmp = await tmpdir()