fix: recover from expired enterprise auth on remote config load (#31661)

This commit is contained in:
Ayush Thakur
2026-06-10 20:32:09 +05:30
committed by GitHub
parent 3ad6923c61
commit 02608a4e97
5 changed files with 67 additions and 7 deletions
+4 -1
View File
@@ -1,3 +1,4 @@
import type { Argv } from "yargs"
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import { CliError, effectCmd, fail } from "../effect-cmd"
@@ -298,7 +299,9 @@ export const ProvidersListCommand = effectCmd({
export const ProvidersLoginCommand = effectCmd({
command: "login [url]",
describe: "log in to a provider",
builder: (yargs) =>
// URL login skips instance bootstrap, which would load remote config with the stale token and crash before re-auth.
instance: (args) => !args.url,
builder: (yargs: Argv) =>
yargs
.positional("url", {
describe: "opencode auth provider",
+12
View File
@@ -94,6 +94,18 @@ export function FormatError(input: unknown): string | undefined {
return stringField(configFrontmatter, "message") ?? ""
}
// ConfigRemoteAuthError: { url: string, remote: string }
const remoteAuth = configData(input, "ConfigRemoteAuthError")
if (remoteAuth) {
const url = stringField(remoteAuth, "url")
const remote = stringField(remoteAuth, "remote")
return [
`Failed to load remote config${remote ? ` from ${remote}` : ""}: the server returned a login page instead of JSON.`,
`Authentication is missing or has expired (the endpoint is likely behind an SSO or identity-aware proxy).`,
...(url ? [`Run \`opencode auth login ${url}\` to re-authenticate.`] : []),
].join("\n")
}
// ConfigInvalidError: { path?: string, message?: string, issues?: Array<{ message: string, path: string[] }> }
const configInvalid = configData(input, "ConfigInvalidError")
if (configInvalid) {
+14 -4
View File
@@ -19,10 +19,11 @@ import type { ConsoleState } from "@opencode-ai/core/v1/config/console-state"
import { FSUtil } from "@opencode-ai/core/fs-util"
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 { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { EffectFlock } from "@opencode-ai/core/util/effect-flock"
import { containsPath, type InstanceContext } from "../project/instance-context"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { RemoteAuthError } from "@opencode-ai/core/v1/config/error"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { ConfigPluginV1 } from "@opencode-ai/core/v1/config/plugin"
import { ConfigAgent } from "./agent"
@@ -187,6 +188,7 @@ export const layer = Layer.effect(
url: string,
headers: Record<string, string> | undefined,
schema: S,
loginOrigin: string,
) {
const response = yield* HttpClient.filterStatusOk(withTransientReadRetry(http))
.execute(
@@ -195,7 +197,15 @@ export const layer = Layer.effect(
.pipe(
Effect.catch((error) => Effect.die(new Error(`failed to fetch remote config from ${url}: ${String(error)}`))),
)
return yield* HttpClientResponse.schemaBodyJson(schema)(response).pipe(
const body = yield* response.text.pipe(
Effect.catch((error) => Effect.die(new Error(`failed to read remote config from ${url}: ${String(error)}`))),
)
// 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)) {
return yield* Effect.die(new RemoteAuthError({ url: loginOrigin, remote: url }))
}
return yield* Schema.decodeEffect(Schema.fromJsonString(schema))(body).pipe(
Effect.catch((error) => Effect.die(new Error(`failed to decode remote config from ${url}: ${String(error)}`))),
)
})
@@ -348,7 +358,7 @@ export const layer = Layer.effect(
authEnv[value.key] = value.token
const wellknownURL = `${url}/.well-known/opencode`
yield* Effect.logDebug("fetching remote config", { url: wellknownURL })
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown)
const wellknown = yield* fetchRemoteJson(wellknownURL, undefined, ConfigV1.WellKnown, url)
const remote = yield* Effect.promise(() =>
substituteWellKnownRemoteConfig({
value: wellknown.remote_config,
@@ -360,7 +370,7 @@ export const layer = Layer.effect(
const fetchedConfig = remote
? yield* Effect.gen(function* () {
yield* Effect.logDebug("fetching remote config", { url: remote.url })
const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json)
const data = yield* fetchRemoteJson(remote.url, remote.headers, Schema.Json, url)
if (isRecord(data) && isRecord(data.config)) return data.config
if (isRecord(data)) return data
return yield* Effect.die(