refactor(opencode): fetch remote config with http client (#28661)

This commit is contained in:
Kit Langton 2026-05-21 15:53:44 -04:00 committed by GitHub
parent 562d299a41
commit b99787e95b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 366 additions and 257 deletions

View file

@ -19,6 +19,7 @@ import type { ConsoleState } from "./console-state"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath, type InstanceContext } from "../project/instance-context"
import { NonNegativeInt, PositiveInt, type DeepMutable } from "@opencode-ai/core/schema"
@ -41,6 +42,7 @@ import { ConfigServer } from "./server"
import { ConfigSkills } from "./skills"
import { ConfigVariable } from "./variable"
import { Npm } from "@opencode-ai/core/npm"
import { withTransientReadRetry } from "@/util/effect-http-client"
const log = Log.create({ service: "config" })
@ -70,14 +72,20 @@ 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
async function substituteWellKnownRemoteConfig(input: {
value: unknown
dir: string
source: string
env: Record<string, string>
}) {
if (!isRecord(input.value) || typeof input.value.url !== "string") return undefined
const url = await ConfigVariable.substitute({
text: input.value.url,
type: "virtual",
dir: input.dir,
source: input.source,
env: input.env,
})
const headers = isRecord(input.value.headers)
? Object.fromEntries(
@ -91,6 +99,7 @@ async function substituteWellKnownRemoteConfig(input: { value: unknown; dir: str
type: "virtual",
dir: input.dir,
source: input.source,
env: input.env,
}),
]),
),
@ -100,6 +109,11 @@ async function substituteWellKnownRemoteConfig(input: { value: unknown; dir: str
return { url, headers }
}
const WellKnownConfig = Schema.Struct({
config: Schema.optional(Schema.Json),
remote_config: Schema.optional(Schema.Json),
})
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++) {
@ -303,7 +317,7 @@ export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
type State = {
config: Info
directories: string[]
deps: Fiber.Fiber<void, never>[]
deps: Fiber.Fiber<void>[]
consoleState: ConsoleState
}
@ -372,17 +386,38 @@ export const layer = Layer.effect(
const accountSvc = yield* Account.Service
const env = yield* Env.Service
const npmSvc = yield* Npm.Service
const http = yield* HttpClient.HttpClient
const readConfigFile = (filepath: string) => fs.readFileStringSafe(filepath).pipe(Effect.orDie)
const fetchRemoteJson = Effect.fnUntraced(function* <S extends Schema.Top>(
url: string,
headers: Record<string, string> | undefined,
schema: S,
) {
const response = yield* HttpClient.filterStatusOk(withTransientReadRetry(http))
.execute(
HttpClientRequest.get(url).pipe(HttpClientRequest.acceptJson, HttpClientRequest.setHeaders(headers ?? {})),
)
.pipe(
Effect.catch((error) => Effect.die(new Error(`failed to fetch remote config from ${url}: ${String(error)}`))),
)
return yield* HttpClientResponse.schemaBodyJson(schema)(response).pipe(
Effect.catch((error) => Effect.die(new Error(`failed to decode remote config from ${url}: ${String(error)}`))),
)
})
const loadConfig = Effect.fnUntraced(function* (
text: string,
options: { path: string } | { dir: string; source: string },
env?: Record<string, string>,
) {
const source = "path" in options ? options.path : options.source
const expanded = yield* Effect.promise(() =>
ConfigVariable.substitute(
"path" in options ? { text, type: "path", path: options.path } : { text, type: "virtual", ...options },
"path" in options
? { text, type: "path", path: options.path, env }
: { text, type: "virtual", ...options, env },
),
)
const parsed = ConfigParse.jsonc(expanded, source)
@ -398,14 +433,14 @@ export const layer = Layer.effect(
return data
})
const loadFile = Effect.fnUntraced(function* (filepath: string) {
const loadFile = Effect.fnUntraced(function* (filepath: string, env?: Record<string, string>) {
log.info("loading", { path: filepath })
const text = yield* readConfigFile(filepath)
if (!text) return {} as Info
return yield* loadConfig(text, { path: filepath })
return yield* loadConfig(text, { path: filepath }, env)
})
const loadGlobal = Effect.fnUntraced(function* () {
const loadGlobal = Effect.fnUntraced(function* (env?: Record<string, string>) {
let result: Info = {}
// Seed the default global config with the schema for editor completion, but avoid writing when the user
// explicitly routes config through env-provided paths or content.
@ -417,9 +452,9 @@ export const layer = Layer.effect(
.pipe(Effect.catch(() => Effect.void))
}
}
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "config.json")))
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.json")))
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.jsonc")))
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "config.json"), env))
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.json"), env))
result = mergeConfig(result, yield* loadFile(path.join(Global.Path.config, "opencode.jsonc"), env))
const legacy = path.join(Global.Path.config, "config")
if (existsSync(legacy)) {
@ -477,6 +512,7 @@ export const layer = Layer.effect(
const auth = yield* authSvc.all().pipe(Effect.orDie)
let result: Info = {}
const authEnv: Record<string, string> = {}
const consoleManagedProviders = new Set<string>()
let activeOrgName: string | undefined
@ -516,56 +552,56 @@ export const layer = Layer.effect(
for (const [key, value] of Object.entries(auth)) {
if (value.type === "wellknown") {
const url = key.replace(/\/+$/, "")
process.env[value.key] = value.token
log.debug("fetching remote config", { url: `${url}/.well-known/opencode` })
const response = yield* Effect.promise(() => fetch(`${url}/.well-known/opencode`))
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>
remote_config?: unknown
}
authEnv[value.key] = value.token
const wellknownURL = `${url}/.well-known/opencode`
log.debug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, WellKnownConfig)
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
value: wellknown.remote_config,
dir: url,
source: `${url}/.well-known/opencode`,
source: wellknownURL,
env: authEnv,
}),
)
const fetchedConfig = remote
? ((yield* Effect.promise(async () => {
? yield* Effect.gen(function* () {
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 data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json)
if (isRecord(data) && isRecord(data.config)) return data.config
if (isRecord(data)) return data
return yield* Effect.die(
new Error(`failed to decode remote config from ${remote.url}: expected object`),
)
})
: {}
const remoteConfig = mergeConfig(wellknown.config ?? {}, fetchedConfig as Info)
const remoteConfig = mergeConfig(isRecord(wellknown.config) ? wellknown.config : {}, fetchedConfig)
if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json"
const source = `${url}/.well-known/opencode`
const next = yield* loadConfig(JSON.stringify(remoteConfig), {
dir: path.dirname(source),
source,
})
const source = wellknownURL
const next = yield* loadConfig(
JSON.stringify(remoteConfig),
{
dir: path.dirname(source),
source,
},
authEnv,
)
yield* merge(source, next, "global")
log.debug("loaded remote config from well-known", { url })
}
}
const global = yield* getGlobal()
const global = Object.keys(authEnv).length ? yield* loadGlobal(authEnv) : yield* getGlobal()
yield* merge(Global.Path.config, global, "global")
if (Flag.OPENCODE_CONFIG) {
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG, authEnv))
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
}
if (!Flag.OPENCODE_DISABLE_PROJECT_CONFIG) {
for (const file of yield* ConfigPaths.files("opencode", ctx.directory, ctx.worktree).pipe(Effect.orDie)) {
yield* merge(file, yield* loadFile(file), "local")
yield* merge(file, yield* loadFile(file, authEnv), "local")
}
}
@ -579,14 +615,14 @@ export const layer = Layer.effect(
log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
}
const deps: Fiber.Fiber<void, never>[] = []
const deps: Fiber.Fiber<void>[] = []
for (const dir of directories) {
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.json", "opencode.jsonc"]) {
const source = path.join(dir, file)
log.debug(`loading config from ${source}`)
yield* merge(source, yield* loadFile(source))
yield* merge(source, yield* loadFile(source, authEnv))
result.agent ??= {}
result.mode ??= {}
result.plugin ??= []
@ -835,6 +871,7 @@ export const defaultLayer = layer.pipe(
Layer.provide(Auth.defaultLayer),
Layer.provide(Account.defaultLayer),
Layer.provide(Npm.defaultLayer),
Layer.provide(FetchHttpClient.layer),
)
export * as Config from "./config"

View file

@ -19,6 +19,7 @@ type ParseSource =
type SubstituteInput = ParseSource & {
text: string
missing?: "error" | "empty"
env?: Record<string, string>
}
function source(input: ParseSource) {
@ -33,7 +34,7 @@ function dir(input: ParseSource) {
export async function substitute(input: SubstituteInput) {
const missing = input.missing ?? "error"
let text = input.text.replace(/\{env:([^}]+)\}/g, (_, varName) => {
return process.env[varName] || ""
return (input.env?.[varName] ?? process.env[varName]) || ""
})
const fileMatches = Array.from(text.matchAll(/\{file:[^}]+\}/g))
@ -46,7 +47,7 @@ export async function substitute(input: SubstituteInput) {
for (const match of fileMatches) {
const token = match[0]
const index = match.index!
const index = match.index
out += text.slice(cursor, index)
const lineStart = text.lastIndexOf("\n", index - 1) + 1

View file

@ -1,6 +1,7 @@
import { expect } from "bun:test"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import path from "path"
import { pathToFileURL } from "url"
import { Agent } from "../../src/agent/agent"
@ -29,6 +30,7 @@ const configLayer = Config.layer.pipe(
Layer.provide(AuthTest.empty),
Layer.provide(AccountTest.empty),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
)
const pluginLayer = Plugin.layer.pipe(
Layer.provide(Bus.layer),

View file

@ -1,5 +1,6 @@
import { test, expect, describe, mock, afterEach, beforeEach } from "bun:test"
import { test, expect, describe, afterEach, beforeEach } from "bun:test"
import { Effect, Exit, Layer, Option } from "effect"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { Config } from "@/config/config"
import { ConfigManaged } from "@/config/managed"
@ -36,35 +37,72 @@ import { Global } from "@opencode-ai/core/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "@/util/filesystem"
import { ConfigPlugin } from "@/config/plugin"
import { Npm } from "@opencode-ai/core/npm"
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
activeOrg: () => Effect.succeed(Option.none()),
})
const emptyAuth = Layer.mock(Auth.Service)({
all: () => Effect.succeed({}),
})
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
const testFlock = EffectFlock.defaultLayer
const noopNpm = Layer.mock(Npm.Service)({
install: () => Effect.void,
add: () => Effect.die("not implemented"),
which: () => Effect.succeed(Option.none()),
})
const layer = Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(emptyAccount),
Layer.provideMerge(infra),
Layer.provide(noopNpm),
const unexpectedHttp = HttpClient.make((request) =>
Effect.die(`unexpected http request: ${request.method} ${request.url}`),
)
const json = (request: Parameters<typeof HttpClientResponse.fromWeb>[0], body: unknown, status = 200) =>
HttpClientResponse.fromWeb(
request,
new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
}),
)
const wellKnownAuth = (url: string) =>
Layer.mock(Auth.Service)({
all: () =>
Effect.succeed({
[url]: new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
}),
})
function remoteConfigClient(input: {
wellKnown: unknown
remote?: unknown
seen: { wellKnown?: string; remote?: string; authorization?: string }
}) {
return HttpClient.make((request) => {
if (request.url.includes(".well-known/opencode")) {
input.seen.wellKnown = request.url
return Effect.succeed(json(request, input.wellKnown))
}
if (input.remote !== undefined && request.url.includes("config.example.com")) {
input.seen.remote = request.url
input.seen.authorization = request.headers.authorization
return Effect.succeed(json(request, input.remote))
}
return Effect.succeed(json(request, {}, 404))
})
}
const configLayer = (
options: {
auth?: Layer.Layer<Auth.Service>
account?: Layer.Layer<Account.Service>
client?: HttpClient.HttpClient
} = {},
) =>
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(options.auth ?? AuthTest.empty),
Layer.provide(options.account ?? AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(Layer.succeed(HttpClient.HttpClient, options.client ?? unexpectedHttp)),
)
const layer = configLayer()
const it = testEffect(layer)
const provideCurrentInstance = <A, E, R>(effect: Effect.Effect<A, E, R>, ctx: InstanceContext) =>
@ -95,6 +133,7 @@ const listDirs = (ctx: InstanceContext) =>
)
// Get managed config directory from environment (set in preload.ts)
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
const originalTestToken = process.env.TEST_TOKEN
beforeEach(async () => {
await clear(true)
@ -102,6 +141,8 @@ beforeEach(async () => {
afterEach(async () => {
await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
if (originalTestToken === undefined) delete process.env.TEST_TOKEN
else process.env.TEST_TOKEN = originalTestToken
await clear(true)
})
@ -528,15 +569,7 @@ test("resolves env templates in account config with account token", async () =>
token: () => Effect.succeed(Option.some(AccessToken.make("st_test_token"))),
})
const layer = Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(fakeAccount),
Layer.provideMerge(infra),
Layer.provide(noopNpm),
)
const layer = configLayer({ account: fakeAccount })
try {
await provideTmpdirInstance(() =>
@ -900,15 +933,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
const prev = process.env.OPENCODE_CONFIG_DIR
process.env.OPENCODE_CONFIG_DIR = tmp.extra
const testLayer = Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(emptyAccount),
Layer.provideMerge(infra),
Layer.provide(noopNpm),
)
const testLayer = configLayer()
try {
await withTestInstance({
@ -1559,42 +1584,79 @@ it.instance("local .opencode config can override MCP from project config", () =>
)
test("project config overrides remote well-known config", async () => {
const originalFetch = globalThis.fetch
let fetchedUrl: string | undefined
globalThis.fetch = mock((url: string | URL | Request) => {
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
if (urlStr.includes(".well-known/opencode")) {
fetchedUrl = urlStr
return Promise.resolve(
new Response(
JSON.stringify({
config: {
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
},
}),
{ status: 200 },
),
)
}
return originalFetch(url)
}) 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 seen: { wellKnown?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
config: {
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: false } },
},
},
})
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),
await provideTmpdirInstance(
() =>
Config.Service.use((svc) =>
Effect.gen(function* () {
const config = yield* svc.get()
expect(seen.wellKnown).toBe("https://example.com/.well-known/opencode")
expect(config.mcp?.jira?.enabled).toBe(true)
}),
),
{
git: true,
config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
},
).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com"), client })),
Effect.runPromise,
)
})
test("wellknown URL with trailing slash is normalized", async () => {
const seen: { wellKnown?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
config: {
mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
},
},
})
await provideTmpdirInstance(
() =>
Config.Service.use((svc) =>
Effect.gen(function* () {
yield* svc.get()
expect(seen.wellKnown).toBe("https://example.com/.well-known/opencode")
}),
),
{ git: true },
).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com/"), client })),
Effect.runPromise,
)
})
test("remote well-known config can use FetchHttpClient layer", async () => {
let fetchedUrl: string | undefined
const server = Bun.serve({
port: 0,
fetch: (request) => {
fetchedUrl = request.url
return new Response(
JSON.stringify({
config: {
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } },
},
}),
{ status: 200, headers: { "content-type": "application/json" } },
)
},
})
try {
await provideTmpdirInstance(
@ -1602,151 +1664,168 @@ test("project config overrides remote well-known config", async () => {
Config.Service.use((svc) =>
Effect.gen(function* () {
const config = yield* svc.get()
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
expect(fetchedUrl).toBe(`${server.url.origin}/.well-known/opencode`)
expect(config.mcp?.jira?.enabled).toBe(true)
}),
),
{
git: true,
config: { mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } } },
},
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
} finally {
globalThis.fetch = originalFetch
}
})
test("wellknown URL with trailing slash is normalized", async () => {
const originalFetch = globalThis.fetch
let fetchedUrl: string | undefined
globalThis.fetch = mock((url: string | URL | Request) => {
const urlStr = url instanceof Request ? url.url : url instanceof URL ? url.href : url
if (urlStr.includes(".well-known/opencode")) {
fetchedUrl = urlStr
return Promise.resolve(
new Response(
JSON.stringify({
config: {
mcp: { slack: { type: "remote", url: "https://slack.example.com/mcp", enabled: true } },
},
}),
{ status: 200 },
),
)
}
return originalFetch(url)
}) 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* () {
yield* svc.get()
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
}),
),
{ git: true },
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
).pipe(
Effect.scoped,
Effect.provide(
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(wellKnownAuth(server.url.origin)),
Layer.provide(AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
),
),
Effect.runPromise,
)
} finally {
globalThis.fetch = originalFetch
await server.stop(true)
}
})
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 seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
remote_config: {
url: "https://config.example.com/opencode.json",
headers: {
Authorization: "Bearer {env:TEST_TOKEN}",
},
},
},
remote: {
mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
},
})
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(seen.wellKnown).toBe("https://example.com/.well-known/opencode")
expect(seen.remote).toBe("https://config.example.com/opencode.json")
expect(seen.authorization).toBe("Bearer test-token")
expect(config.mcp?.confluence?.enabled).toBe(true)
}),
),
{ git: true },
).pipe(Effect.scoped, Effect.provide(layer), Effect.runPromise)
).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com"), client })),
Effect.runPromise,
)
} finally {
globalThis.fetch = originalFetch
if (originalToken === undefined) delete process.env.TEST_TOKEN
else process.env.TEST_TOKEN = originalToken
}
})
test("wellknown token env substitution does not mutate process env", async () => {
const originalToken = process.env.TEST_TOKEN
process.env.TEST_TOKEN = "preexisting-token"
const seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
remote_config: {
url: "https://config.example.com/opencode.json",
headers: {
Authorization: "Bearer {env:TEST_TOKEN}",
},
},
},
remote: {
mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
},
})
try {
const config = await provideTmpdirInstance(() => Config.Service.use((svc) => svc.get()), {
git: true,
config: { username: "{env:TEST_TOKEN}" },
}).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com"), client })),
Effect.runPromise,
)
expect(seen.authorization).toBe("Bearer test-token")
expect(config.username).toBe("test-token")
expect(process.env.TEST_TOKEN).toBe("preexisting-token")
} finally {
if (originalToken === undefined) delete process.env.TEST_TOKEN
else process.env.TEST_TOKEN = originalToken
}
})
test("wellknown config null is treated as absent", async () => {
const seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
config: null,
remote_config: {
url: "https://config.example.com/opencode.json",
},
},
remote: {
mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } },
},
})
await provideTmpdirInstance(
() =>
Config.Service.use((svc) =>
Effect.gen(function* () {
const config = yield* svc.get()
expect(seen.remote).toBe("https://config.example.com/opencode.json")
expect(config.mcp?.confluence?.enabled).toBe(true)
}),
),
{ git: true },
).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com"), client })),
Effect.runPromise,
)
})
test("wellknown remote_config rejects non-object config responses", async () => {
const seen: { wellKnown?: string; remote?: string; authorization?: string } = {}
const client = remoteConfigClient({
seen,
wellKnown: {
remote_config: {
url: "https://config.example.com/opencode.json",
},
},
remote: "not an object",
})
const exit = await provideTmpdirInstance(() => Config.Service.use((svc) => svc.get()).pipe(Effect.exit), {
git: true,
}).pipe(
Effect.scoped,
Effect.provide(configLayer({ auth: wellKnownAuth("https://example.com"), client })),
Effect.runPromise,
)
expect(seen.remote).toBe("https://config.example.com/opencode.json")
expect(Exit.isFailure(exit)).toBe(true)
})
describe("resolvePluginSpec", () => {
test("keeps package specs unchanged", async () => {
await using tmp = await tmpdir()

View file

@ -1,12 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import path from "path"
import { pathToFileURL } from "url"
import { Account } from "../../src/account/account"
import { Auth } from "../../src/auth"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config"
import { Env } from "../../src/env"
@ -15,22 +14,18 @@ import { Plugin } from "../../src/plugin/index"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
activeOrg: () => Effect.succeed(Option.none()),
})
const emptyAuth = Layer.mock(Auth.Service)({
all: () => Effect.succeed({}),
})
const configLayer = Config.layer.pipe(
Layer.provide(EffectFlock.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(emptyAccount),
Layer.provide(AuthTest.empty),
Layer.provide(AccountTest.empty),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
)
const it = testEffect(
Layer.mergeAll(

View file

@ -1,12 +1,11 @@
import { afterEach, describe, expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { Effect, Layer } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import path from "path"
import { pathToFileURL } from "url"
import { Account } from "../../src/account/account"
import { Auth } from "../../src/auth"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config/config"
@ -24,22 +23,18 @@ import { SessionPrompt } from "../../src/session/prompt"
import { SyncEvent } from "../../src/sync"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
const emptyAccount = Layer.mock(Account.Service)({
active: () => Effect.succeed(Option.none()),
activeOrg: () => Effect.succeed(Option.none()),
})
const emptyAuth = Layer.mock(Auth.Service)({
all: () => Effect.succeed({}),
})
const configLayer = Config.layer.pipe(
Layer.provide(EffectFlock.defaultLayer),
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(emptyAuth),
Layer.provide(emptyAccount),
Layer.provide(AuthTest.empty),
Layer.provide(AccountTest.empty),
Layer.provide(NpmTest.noop),
Layer.provide(FetchHttpClient.layer),
)
const pluginLayer = Plugin.layer.pipe(
Layer.provide(Bus.layer),