Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,16 +189,33 @@ export const layer = Layer.effect(
schema: S,
loginOrigin: string,
) {
// Transport-level failures (DNS/connection/timeout, non-2xx status, body
// read failures) always degrade to a warning — an unreachable well-known
// source cannot crash config loading. Auth errors (RemoteAuthError) and
// decode failures stay hard errors: they indicate a broken credential or
// a malformed remote config, not an offline environment.
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)}`))),
Effect.catch((error) =>
Effect.logWarning("failed to fetch remote config, skipping source", {
url,
reason: String(error),
}).pipe(Effect.as(undefined)),
),
)
if (response === undefined) return undefined
const body = yield* response.text.pipe(
Effect.catch((error) => Effect.die(new Error(`failed to read remote config from ${url}: ${String(error)}`))),
Effect.catch((error) =>
Effect.logWarning("failed to read remote config, skipping source", {
url,
reason: String(error),
}).pipe(Effect.as(undefined)),
),
)
if (body === undefined) return undefined
// An auth proxy can answer with an HTML login page at HTTP 200 (passes filterStatusOk); treat it as a re-auth error, not a decode failure.
const contentType = (response.headers["content-type"] ?? "").toLowerCase()
if (contentType.includes("html") || /^\s*<!doctype|^\s*<html/i.test(body)) {
Expand Down Expand Up @@ -358,6 +375,9 @@ export const layer = Layer.effect(
const wellknownURL = `${url}/.well-known/opencode`
yield* Effect.logDebug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown, url)
// Unreachable source: the warning was logged by fetchRemoteJson; skip this source entirely
// without merging anything so the local config stays fully usable.
if (wellknown === undefined) continue
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
value: wellknown.remote_config,
Expand All @@ -370,6 +390,7 @@ export const layer = Layer.effect(
? yield* Effect.gen(function* () {
yield* Effect.logDebug("fetching remote config", { url: remote.url })
const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json, url)
if (data === undefined) return {}
if (isRecord(data) && isRecord(data.config)) return data.config
if (isRecord(data)) return data
return yield* Effect.die(
Expand Down
234 changes: 234 additions & 0 deletions packages/opencode/test/config/wellknown-offline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import * as TestConsole from "effect/testing/TestConsole"
import { NodeFileSystem, NodePath } from "@effect/platform-node"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/unstable/http"

import { Config } from "@/config/config"
import { Auth } from "../../src/auth"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"

Check warning on line 13 in packages/opencode/test/config/wellknown-offline.test.ts

View workflow job for this annotation

GitHub Actions / Typecheck

eslint(no-unused-vars)

Identifier 'AuthTest' is imported but never used.
import { NpmTest } from "../fake/npm"
import { Env } from "../../src/env"
import { testEffect } from "../lib/effect"

const infra = CrossSpawnSpawner.defaultLayer.pipe(
Layer.provideMerge(Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)),
)

const testFlock = EffectFlock.defaultLayer

const wellKnownAuth = (url: string) =>
Layer.mock(Auth.Service)({
all: () =>
Effect.succeed({
[url]: new Auth.WellKnown({ type: "wellknown", key: "TEST_TOKEN", token: "test-token" }),
}),
})

const configLayer = (client: HttpClient.HttpClient) =>
Config.layer.pipe(
Layer.provide(testFlock),
Layer.provide(Env.defaultLayer),
Layer.provide(wellKnownAuth("https://example.com")),
Layer.provide(AccountTest.empty),
Layer.provideMerge(infra),
Layer.provide(NpmTest.noop),
Layer.provide(Layer.succeed(HttpClient.HttpClient, client)),
Layer.provideMerge(FSUtil.defaultLayer),
)

const it = (client: HttpClient.HttpClient) => testEffect(configLayer(client))

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 transportFailure = (request: Parameters<typeof HttpClientResponse.fromWeb>[0], description: string) =>
Effect.fail(
new HttpClientError.HttpClientError({
reason: new HttpClientError.TransportError({ request, description }),
}),
)

// Well-known endpoint unreachable (DNS/connection failure): the transport never answers.
const unreachable = HttpClient.make((request) => transportFailure(request, "connect ECONNREFUSED"))

// Well-known endpoint answers, but the remote_config URL is unreachable.
const remoteConfigUnreachable = (seen: { wellKnown?: string; remote?: string }) =>
HttpClient.make((request) => {
if (request.url.includes(".well-known/opencode")) {
seen.wellKnown = request.url
return Effect.succeed(
json(request, {
config: { model: "embedded/model" },
remote_config: { url: "https://config.example.com/opencode.json" },
}),
)
}
if (request.url.includes("config.example.com")) {
seen.remote = request.url
return transportFailure(request, "connect timeout")
}
return Effect.succeed(json(request, {}, 404))
})

// Both hops succeed: remote config must merge exactly as before.
const remoteOk = (seen: { wellKnown?: string; remote?: string }) =>
HttpClient.make((request) => {
if (request.url.includes(".well-known/opencode")) {
seen.wellKnown = request.url
return Effect.succeed(json(request, { remote_config: { url: "https://config.example.com/opencode.json" } }))
}
if (request.url.includes("config.example.com")) {
seen.remote = request.url
return Effect.succeed(
json(request, {
config: { mcp: { confluence: { type: "remote", url: "https://confluence.example.com/mcp", enabled: true } } },
}),
)
}
return Effect.succeed(json(request, {}, 404))
})

// Gateway answers the remote_config URL with an HTML login page (auth proxy, not an offline failure).
const loginPage = (seen: { wellKnown?: string; remote?: string }) =>
HttpClient.make((request) => {
if (request.url.includes(".well-known/opencode")) {
seen.wellKnown = request.url
return Effect.succeed(json(request, { remote_config: { url: "https://config.example.com/opencode.json" } }))
}
if (request.url.includes("config.example.com")) {
seen.remote = request.url
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("<!DOCTYPE html><html><head><title>Sign in</title></head><body>Login required</body></html>", {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
}),
),
)
}
return Effect.succeed(json(request, {}, 404))
})

// Well-known endpoint answers 200-OK with JSON content-type, but the body stream
// errors mid-read (truncated transfer / connection reset). Exercises the body-read
// degrade path (config.ts:210-217) distinct from the transport-level fetch degrade.
const bodyReadFails = HttpClient.make((request) => {
if (request.url.includes(".well-known/opencode")) {
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(
new ReadableStream({
start(controller) {
controller.error(new Error("body stream interrupted"))
},
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
),
)
}
return Effect.succeed(json(request, {}, 404))
})

const unreachableIt = it(unreachable)

unreachableIt.instance(
"wellknown transport failure degrades: config loads, local config intact, warning logged",
() =>
Effect.gen(function* () {
const config = yield* Config.use.get()
expect(config.model).toBe("local/model")
expect(config.mcp?.jira?.enabled).toBe(true)
const logs = JSON.stringify(yield* TestConsole.logLines)
expect(logs).toContain("failed to fetch remote config")
expect(logs).toContain("https://example.com/.well-known/opencode")
}),
{
config: {
model: "local/model",
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } },
},
},
)

const remoteUnreachableSeen: { wellKnown?: string; remote?: string } = {}
const remoteUnreachableIt = it(remoteConfigUnreachable(remoteUnreachableSeen))

remoteUnreachableIt.instance(
"remote_config transport failure degrades: embedded wellknown config still merges, warning logged",
() =>
Effect.gen(function* () {
const config = yield* Config.use.get()
expect(remoteUnreachableSeen.wellKnown).toBe("https://example.com/.well-known/opencode")
expect(remoteUnreachableSeen.remote).toBe("https://config.example.com/opencode.json")
expect(config.model).toBe("embedded/model")
const logs = JSON.stringify(yield* TestConsole.logLines)
expect(logs).toContain("failed to fetch remote config")
expect(logs).toContain("https://config.example.com/opencode.json")
}),
)

const remoteOkSeen: { wellKnown?: string; remote?: string } = {}
const remoteOkIt = it(remoteOk(remoteOkSeen))

remoteOkIt.instance(
"success path unchanged: remote config merges, no degradation warning",
() =>
Effect.gen(function* () {
const config = yield* Config.use.get()
expect(remoteOkSeen.wellKnown).toBe("https://example.com/.well-known/opencode")
expect(remoteOkSeen.remote).toBe("https://config.example.com/opencode.json")
expect(config.mcp?.confluence?.enabled).toBe(true)
expect(JSON.stringify(yield* TestConsole.logLines)).not.toContain("failed to fetch remote config")
}),
)

const loginPageSeen: { wellKnown?: string; remote?: string } = {}
const loginPageIt = it(loginPage(loginPageSeen))

loginPageIt.instance(
"HTML login page stays a hard auth failure even with degradation enabled",
() =>
Effect.gen(function* () {
const exit = yield* Config.use.get().pipe(Effect.exit)
expect(loginPageSeen.remote).toBe("https://config.example.com/opencode.json")
expect(Exit.isFailure(exit)).toBe(true)
}),
)

const bodyReadFailsIt = it(bodyReadFails)

bodyReadFailsIt.instance(
"wellknown body-read failure degrades: config loads, local config intact, warning logged",
() =>
Effect.gen(function* () {
const config = yield* Config.use.get()
expect(config.model).toBe("local/model")
expect(config.mcp?.jira?.enabled).toBe(true)
const logs = JSON.stringify(yield* TestConsole.logLines)
expect(logs).toContain("failed to read remote config")
expect(logs).toContain("https://example.com/.well-known/opencode")
}),
{
config: {
model: "local/model",
mcp: { jira: { type: "remote", url: "https://jira.example.com/mcp", enabled: true } },
},
},
)
Loading