refactor(flags): move embedded web ui flag to runtime flags (#27613)

This commit is contained in:
Shoubhit Dash
2026-05-15 03:51:29 +05:30
committed by GitHub
parent e22cfa435a
commit 43310f4d8c
6 changed files with 129 additions and 62 deletions

View File

@@ -62,7 +62,6 @@ export const Flag = {
OPENCODE_ENABLE_PARALLEL: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"],
OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"],
OPENCODE_DISABLE_EMBEDDED_WEB_UI: truthy("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
OPENCODE_DB: process.env["OPENCODE_DB"],
OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"),
OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"),

View File

@@ -14,6 +14,7 @@ const enabledByExperimental = (name: string) =>
export class Service extends ConfigService.Service<Service>()("@opencode/RuntimeFlags", {
pure: bool("OPENCODE_PURE"),
disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"),
disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"),
disableClaudeCodeSkills: Config.all({
broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"),
direct: bool("OPENCODE_DISABLE_CLAUDE_CODE_SKILLS"),

View File

@@ -21,6 +21,7 @@ import { File } from "@/file"
import { FileWatcher } from "@/file/watcher"
import { Ripgrep } from "@/file/ripgrep"
import { Format } from "@/format"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { LSP } from "@/lsp/lsp"
import { MCP } from "@/mcp"
import { Permission } from "@/permission"
@@ -172,7 +173,10 @@ const uiRoute = HttpRouter.use((router) =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const client = yield* HttpClient.HttpClient
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
const flags = yield* RuntimeFlags.Service
yield* router.add("*", "/*", (request) =>
serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }),
)
}),
).pipe(Layer.provide(authOnlyRouterLayer))
@@ -206,6 +210,7 @@ export function createRoutes(corsOptions?: CorsOptions) {
PtyTicket.defaultLayer,
Question.defaultLayer,
Ripgrep.defaultLayer,
RuntimeFlags.defaultLayer,
Session.defaultLayer,
SessionCompaction.defaultLayer,
SessionPrompt.defaultLayer,

View File

@@ -1,14 +1,10 @@
import { Flag } from "@opencode-ai/core/flag/flag"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Effect, Stream } from "effect"
import { HttpBody, HttpClient, HttpClientRequest, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { createHash } from "node:crypto"
import { ProxyUtil } from "../proxy-util"
const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
? Promise.resolve(null)
: // @ts-expect-error - generated file at build time
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
let embeddedUIPromise: Promise<Record<string, string> | null> | undefined
export const UI_UPSTREAM = new URL("https://app.opencode.ai")
@@ -45,9 +41,11 @@ export function upstreamURL(path: string) {
return new URL(path, UI_UPSTREAM).toString()
}
export function embeddedUI() {
if (Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI) return Promise.resolve(null)
return embeddedUIPromise
export function embeddedUI(disableEmbeddedWebUi: boolean) {
if (disableEmbeddedWebUi) return Promise.resolve(null)
return (embeddedUIPromise ??=
// @ts-expect-error - generated file at build time
import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null))
}
function notFound() {
@@ -79,10 +77,10 @@ export function serveEmbeddedUIEffect(
export function serveUIEffect(
request: HttpServerRequest.HttpServerRequest,
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient },
services: { fs: AppFileSystem.Interface; client: HttpClient.HttpClient; disableEmbeddedWebUi: boolean },
) {
return Effect.gen(function* () {
const embeddedWebUI = yield* Effect.promise(() => embeddedUI())
const embeddedWebUI = yield* Effect.promise(() => embeddedUI(services.disableEmbeddedWebUi))
const path = new URL(request.url, "http://localhost").pathname
if (embeddedWebUI) return yield* serveEmbeddedUIEffect(path, services.fs, embeddedWebUI)

View File

@@ -16,6 +16,7 @@ describe("RuntimeFlags", () => {
fromConfig({
OPENCODE_PURE: "true",
OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true",
OPENCODE_EXPERIMENTAL: "true",
OPENCODE_ENABLE_EXA: "true",
OPENCODE_ENABLE_PARALLEL: "true",
@@ -28,6 +29,7 @@ describe("RuntimeFlags", () => {
expect(flags.pure).toBe(true)
expect(flags.disableDefaultPlugins).toBe(true)
expect(flags.disableEmbeddedWebUi).toBe(true)
expect(flags.enableExa).toBe(true)
expect(flags.enableParallel).toBe(true)
expect(flags.enableExperimentalModels).toBe(true)
@@ -67,6 +69,7 @@ describe("RuntimeFlags", () => {
expect(flags.pure).toBe(false)
expect(flags.disableDefaultPlugins).toBe(true)
expect(flags.disableEmbeddedWebUi).toBe(false)
expect(flags.disableClaudeCodeSkills).toBe(false)
expect(flags.enableExa).toBe(false)
expect(flags.experimentalIconDiscovery).toBe(false)
@@ -186,6 +189,7 @@ describe("RuntimeFlags", () => {
expect(flags.pure).toBe(false)
expect(flags.disableDefaultPlugins).toBe(false)
expect(flags.disableEmbeddedWebUi).toBe(false)
expect(flags.disableClaudeCodeSkills).toBe(false)
expect(flags.enableExa).toBe(false)
expect(flags.experimentalIconDiscovery).toBe(false)

View File

@@ -13,11 +13,11 @@ import {
HttpServerResponse,
} from "effect/unstable/http"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { RuntimeFlags } from "../../src/effect/runtime-flags"
import { ServerAuth } from "../../src/server/auth"
import { authorizationRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/authorization"
import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server"
import { serveEmbeddedUIEffect, serveUIEffect } from "../../src/server/shared/ui"
import { Server } from "../../src/server/server"
import { testEffect } from "../lib/effect"
void Log.init({ print: false })
@@ -25,7 +25,6 @@ void Log.init({ print: false })
const testStateLayer = Layer.effectDiscard(
Effect.gen(function* () {
const original = {
OPENCODE_DISABLE_EMBEDDED_WEB_UI: Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI,
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
envPassword: process.env.OPENCODE_SERVER_PASSWORD,
@@ -34,7 +33,6 @@ const testStateLayer = Layer.effectDiscard(
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = original.OPENCODE_DISABLE_EMBEDDED_WEB_UI
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
restoreEnv("OPENCODE_SERVER_PASSWORD", original.envPassword)
@@ -44,7 +42,7 @@ const testStateLayer = Layer.effectDiscard(
}),
)
const it = testEffect(Layer.mergeAll(testStateLayer, AppFileSystem.defaultLayer))
const it = testEffect(Layer.mergeAll(testStateLayer, AppFileSystem.defaultLayer, RuntimeFlags.layer()))
function restoreEnv(key: string, value: string | undefined) {
if (value === undefined) {
@@ -82,19 +80,28 @@ function app(input?: { password?: string; username?: string }) {
}
}
function uiApp(input?: { password?: string; username?: string; client?: Layer.Layer<HttpClient.HttpClient> }) {
function uiApp(input?: {
password?: string
username?: string
client?: Layer.Layer<HttpClient.HttpClient>
disableEmbeddedWebUi?: boolean
}) {
const handler = HttpRouter.toWebHandler(
HttpRouter.use((router) =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const client = yield* HttpClient.HttpClient
yield* router.add("*", "/*", (request) => serveUIEffect(request, { fs, client }))
const flags = yield* RuntimeFlags.Service
yield* router.add("*", "/*", (request) =>
serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }),
)
}),
).pipe(
Layer.provide(authorizationRouterMiddleware.layer.pipe(Layer.provide(ServerAuth.Config.defaultLayer))),
Layer.provide([
AppFileSystem.defaultLayer,
input?.client ?? httpClient(new Response("ui")),
RuntimeFlags.layer({ disableEmbeddedWebUi: input?.disableEmbeddedWebUi ?? false }),
HttpServer.layerServices,
ConfigProvider.layer(
ConfigProvider.fromUnknown({
@@ -120,6 +127,48 @@ function uiApp(input?: { password?: string; username?: string; client?: Layer.La
}
}
function routeOrderingApp() {
let proxiedUrl: string | undefined
const handler = HttpRouter.toWebHandler(
HttpRouter.use((router) =>
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const client = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
yield* router.add("GET", "/session/:sessionID", () =>
Effect.succeed(HttpServerResponse.jsonUnsafe({ error: "Not Found" }, { status: 404 })),
)
yield* router.add("*", "/*", (request) =>
serveUIEffect(request, { fs, client, disableEmbeddedWebUi: flags.disableEmbeddedWebUi }),
)
}),
).pipe(
Layer.provide([
AppFileSystem.defaultLayer,
RuntimeFlags.layer({ disableEmbeddedWebUi: true }),
httpClient(new Response("ui"), (request) => {
proxiedUrl = request.url
}),
HttpServer.layerServices,
]),
),
{ disableLogger: true },
).handler
return {
proxiedUrl: () => proxiedUrl,
request(input: string | URL | Request, init?: RequestInit) {
return Effect.promise(() =>
Promise.resolve(
handler(
input instanceof Request ? input : new Request(new URL(input, "http://localhost"), init),
ExperimentalHttpApiServer.context,
),
),
)
},
}
}
function httpClient(response: Response, onRequest?: (request: HttpClientRequest.HttpClientRequest) => void) {
return Layer.succeed(
HttpClient.HttpClient,
@@ -137,10 +186,10 @@ function responseText(response: Response) {
describe("HttpApi UI fallback", () => {
it.live("serves the web UI through the experimental backend", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
let proxiedUrl: string | undefined
const response = yield* uiApp({
disableEmbeddedWebUi: true,
client: httpClient(
new Response("<html>opencode</html>", { headers: { "content-type": "text/html" } }),
(request) => {
@@ -158,35 +207,39 @@ describe("HttpApi UI fallback", () => {
it.live("strips upstream transfer encoding headers from proxied assets", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
let proxiedUrl: string | undefined
const response = yield* Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const client = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/assets/app.js")), {
fs,
client,
disableEmbeddedWebUi: flags.disableEmbeddedWebUi,
})
}).pipe(
Effect.provide(
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
proxiedUrl = request.url
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("console.log('ok')", {
headers: {
"content-encoding": "br",
"content-length": "999",
"content-type": "text/javascript",
},
}),
),
)
}),
Layer.mergeAll(
RuntimeFlags.layer({ disableEmbeddedWebUi: true }),
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => {
proxiedUrl = request.url
return Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("console.log('ok')", {
headers: {
"content-encoding": "br",
"content-length": "999",
"content-type": "text/javascript",
},
}),
),
)
}),
),
),
),
Effect.map(HttpServerResponse.toWeb),
@@ -206,29 +259,32 @@ describe("HttpApi UI fallback", () => {
// causing browsers to fail with `ERR_INVALID_CHUNKED_ENCODING`.
it.live("strips upstream transfer-encoding header from proxied assets", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
const response = yield* Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const client = yield* HttpClient.HttpClient
const flags = yield* RuntimeFlags.Service
return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/")), {
fs,
client,
disableEmbeddedWebUi: flags.disableEmbeddedWebUi,
})
}).pipe(
Effect.provide(
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("<html>opencode</html>", {
headers: {
"transfer-encoding": "chunked",
"content-type": "text/html",
},
}),
Layer.mergeAll(
RuntimeFlags.layer({ disableEmbeddedWebUi: true }),
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) =>
Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response("<html>opencode</html>", {
headers: {
"transfer-encoding": "chunked",
"content-type": "text/html",
},
}),
),
),
),
),
@@ -301,17 +357,21 @@ describe("HttpApi UI fallback", () => {
it.live("keeps matched API routes ahead of the UI fallback", () =>
Effect.gen(function* () {
const response = yield* Effect.promise(() => Promise.resolve(Server.Default().app.request("/session/ses_nope")))
const server = routeOrderingApp()
const response = yield* server.request("/session/ses_nope")
expect(response.status).toBe(404)
expect(server.proxiedUrl()).toBeUndefined()
}),
)
it.live("requires server password for the web UI", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
const response = yield* uiApp({ password: "secret", username: "opencode" }).request("/")
const response = yield* uiApp({
password: "secret",
username: "opencode",
disableEmbeddedWebUi: true,
}).request("/")
expect(response.status).toBe(401)
expect(response.headers.get("www-authenticate")).toBe('Basic realm="Secure Area"')
@@ -320,11 +380,10 @@ describe("HttpApi UI fallback", () => {
it.live("accepts auth token for the web UI", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
const response = yield* uiApp({
password: "secret",
username: "opencode",
disableEmbeddedWebUi: true,
client: httpClient(new Response("<html>opencode</html>", { headers: { "content-type": "text/html" } })),
}).request(`/?auth_token=${btoa("opencode:secret")}`)
@@ -335,9 +394,11 @@ describe("HttpApi UI fallback", () => {
it.live("accepts basic auth for the web UI", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
const response = yield* uiApp({ password: "secret", username: "opencode" }).request("/", {
const response = yield* uiApp({
password: "secret",
username: "opencode",
disableEmbeddedWebUi: true,
}).request("/", {
headers: { authorization: `Basic ${btoa("opencode:secret")}` },
})
@@ -352,12 +413,11 @@ describe("HttpApi UI fallback", () => {
// should bypass auth.
it.live("serves the PWA manifest without auth even when a server password is set", () =>
Effect.gen(function* () {
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
for (const path of ["/site.webmanifest", "/web-app-manifest-192x192.png", "/web-app-manifest-512x512.png"]) {
const response = yield* uiApp({
password: "secret",
username: "opencode",
disableEmbeddedWebUi: true,
client: httpClient(new Response("ok")),
}).request(path)
expect(response.status).not.toBe(401)