feat(config): support well-known remote_config (#26054)
This commit is contained in:
@@ -70,6 +70,40 @@ function normalizeLoadedConfig(data: unknown, source: string) {
|
|||||||
return copy
|
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<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
|
async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
|
||||||
if (!config.plugin) return config
|
if (!config.plugin) return config
|
||||||
for (let i = 0; i < config.plugin.length; i++) {
|
for (let i = 0; i < config.plugin.length; i++) {
|
||||||
@@ -494,8 +528,27 @@ export const layer = Layer.effect(
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
|
throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
|
||||||
}
|
}
|
||||||
const wellknown = (yield* Effect.promise(() => response.json())) as { config?: Record<string, unknown> }
|
const wellknown = (yield* Effect.promise(() => response.json())) as {
|
||||||
const remoteConfig = wellknown.config ?? {}
|
config?: Record<string, unknown>
|
||||||
|
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<string, unknown>)
|
||||||
|
: {}
|
||||||
|
const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info)
|
||||||
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
|
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
|
||||||
const source = `${url}/.well-known/opencode`
|
const source = `${url}/.well-known/opencode`
|
||||||
const next = yield* loadConfig(JSON.stringify(remoteConfig), {
|
const next = yield* loadConfig(JSON.stringify(remoteConfig), {
|
||||||
|
|||||||
@@ -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", () => {
|
describe("resolvePluginSpec", () => {
|
||||||
test("keeps package specs unchanged", async () => {
|
test("keeps package specs unchanged", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
|
|||||||
Reference in New Issue
Block a user