Skip to content

Commit 94c1462

Browse files
thdxrcapyBearista
authored andcommitted
feat(config): support well-known remote_config (anomalyco#26054)
1 parent f979b02 commit 94c1462

2 files changed

Lines changed: 132 additions & 2 deletions

File tree

packages/opencode/src/config/config.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,40 @@ function normalizeLoadedConfig(data: unknown, source: string) {
7070
return copy
7171
}
7272

73+
async function substituteWellKnownRemoteConfig(input: {
74+
value: unknown
75+
dir: string
76+
source: string
77+
}) {
78+
if (!isRecord(input.value) || typeof input.value.url !== "string") return
79+
80+
const url = await ConfigVariable.substitute({
81+
text: input.value.url,
82+
type: "virtual",
83+
dir: input.dir,
84+
source: input.source,
85+
})
86+
const headers = isRecord(input.value.headers)
87+
? Object.fromEntries(
88+
await Promise.all(
89+
Object.entries(input.value.headers)
90+
.filter((entry): entry is [string, string] => typeof entry[1] === "string")
91+
.map(async ([key, value]) => [
92+
key,
93+
await ConfigVariable.substitute({
94+
text: value,
95+
type: "virtual",
96+
dir: input.dir,
97+
source: input.source,
98+
}),
99+
]),
100+
),
101+
)
102+
: undefined
103+
104+
return { url, headers }
105+
}
106+
73107
async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
74108
if (!config.plugin) return config
75109
for (let i = 0; i < config.plugin.length; i++) {
@@ -494,8 +528,27 @@ export const layer = Layer.effect(
494528
if (!response.ok) {
495529
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
496530
}
497-
const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
498-
const remoteConfig = wellknown.config ?? {}
531+
const wellknown = (yield* Effect.promise(() => response.json())) as {
532+
config?: Record<string, unknown>
533+
remote_config?: unknown
534+
}
535+
const remote = yield* Effect.promise(() =>
536+
substituteWellKnownRemoteConfig({
537+
value: wellknown.remote_config,
538+
dir: url,
539+
source: `${url}/.well-known/opencode`,
540+
}),
541+
)
542+
const fetchedConfig = remote
543+
? ((yield* Effect.promise(async () => {
544+
log.debug("fetching remote config", { url: remote.url })
545+
const response = await fetch(remote.url, { headers: remote.headers })
546+
if (!response.ok) throw new Error(`failed to fetch remote config from ${remote.url}: ${response.status}`)
547+
const data = await response.json()
548+
return isRecord(data) && isRecord(data.config) ? data.config : data
549+
})) as Record<string, unknown>)
550+
: {}
551+
const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info)
499552
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
500553
const source = `${url}/.well-known/opencode`
501554
const next = yield* loadConfig(JSON.stringify(remoteConfig), {

packages/opencode/test/config/config.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1972,6 +1972,83 @@ test("wellknown URL with trailing slash is normalized", async () => {
19721972
}
19731973
})
19741974

1975+
test("wellknown remote_config supports templated env vars in headers", async () => {
1976+
const originalFetch = globalThis.fetch
1977+
const originalToken = process.env.TEST_TOKEN
1978+
let wellknownFetchedUrl: string | undefined
1979+
let remoteFetchedUrl: string | undefined
1980+
let remoteHeaders: HeadersInit | undefined
1981+
globalThis.fetch = mock((url: string | URL | Request, init?: RequestInit) => {
1982+
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
1983+
if (urlStr.includes(".well-known/opencode")) {
1984+
wellknownFetchedUrl = urlStr
1985+
return Promise.resolve(
1986+
new Response(
1987+
JSON.stringify({
1988+
remote_config: {
1989+
url: "https://config.example.com/opencode.json",
1990+
headers: {
1991+
Authorization: "Bearer {env:TEST_TOKEN}",
1992+
},
1993+
},
1994+
}),
1995+
{ status: 200 },
1996+
),
1997+
)
1998+
}
1999+
if (urlStr.includes("config.example.com")) {
2000+
remoteFetchedUrl = urlStr
2001+
remoteHeaders = init?.headers
2002+
return Promise.resolve(
2003+
new Response(
2004+
JSON.stringify({
2005+
mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
2006+
}),
2007+
{ status: 200 },
2008+
),
2009+
)
2010+
}
2011+
return originalFetch(url, init)
2012+
}) as unknown as typeof fetch
2013+
2014+
const fakeAuth = Layer.mock(Auth.Service)({
2015+
all: () =>
2016+
Effect.succeed({
2017+
"https://example.com": new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
2018+
}),
2019+
})
2020+
2021+
const layer = Config.layer.pipe(
2022+
Layer.provide(testFlock),
2023+
Layer.provide(AppFileSystem.defaultLayer),
2024+
Layer.provide(Env.defaultLayer),
2025+
Layer.provide(fakeAuth),
2026+
Layer.provide(emptyAccount),
2027+
Layer.provideMerge(infra),
2028+
Layer.provide(noopNpm),
2029+
)
2030+
2031+
try {
2032+
await provideTmpdirInstance(
2033+
() =>
2034+
Config.Service.use((svc) =>
2035+
Effect.gen(function* () {
2036+
const config = yield* svc.get()
2037+
expect(wellknownFetchedUrl).toBe("https://example.com/.well-known/opencode")
2038+
expect(remoteFetchedUrl).toBe("https://config.example.com/opencode.json")
2039+
expect(remoteHeaders).toEqual({ Authorization: "Bearer test-token" })
2040+
expect(config.mcp?.confluence?.enabled).toBe(true)
2041+
}),
2042+
),
2043+
{ git: true },
2044+
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
2045+
} finally {
2046+
globalThis.fetch = originalFetch
2047+
if (originalToken === undefined) delete process.env.TEST_TOKEN
2048+
else process.env.TEST_TOKEN = originalToken
2049+
}
2050+
})
2051+
19752052
describe("resolvePluginSpec", () => {
19762053
test("keeps package specs unchanged", async () => {
19772054
await using tmp = await tmpdir()

0 commit comments

Comments
 (0)