mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-07 00:51:34 +00:00
feat(config): support well-known remote_config (#26054)
This commit is contained in:
parent
63a175b50d
commit
d9c18381a6
2 changed files with 132 additions and 2 deletions
|
|
@ -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<T extends { plugin?: ConfigPlugin.Spec[] }>(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<string, unknown> }
|
||||
const remoteConfig = wellknown.config ?? {}
|
||||
const wellknown = (yield* Effect.promise(() => response.json())) as {
|
||||
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"
|
||||
const source = `${url}/.well-known/opencode`
|
||||
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", () => {
|
||||
test("keeps package specs unchanged", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue