From b7c6fa611fb40bc12d7176c8bb092a816fdb1e99 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 12 May 2026 20:59:02 -0400 Subject: [PATCH] effect: add RuntimeFlags service (#27181) --- packages/opencode/src/effect/runtime-flags.ts | 27 +++++++++ packages/opencode/src/plugin/index.ts | 15 +++-- .../agent/plugin-agent-regression.test.ts | 7 ++- .../test/effect/runtime-flags.test.ts | 55 +++++++++++++++++++ .../test/plugin/auth-override.test.ts | 2 + .../test/plugin/loader-shared.test.ts | 44 ++++----------- packages/opencode/test/plugin/trigger.test.ts | 7 ++- .../test/plugin/workspace-adapter.test.ts | 7 ++- packages/opencode/test/preload.ts | 1 - 9 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 packages/opencode/src/effect/runtime-flags.ts create mode 100644 packages/opencode/test/effect/runtime-flags.test.ts diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts new file mode 100644 index 0000000000..5f07dc6acc --- /dev/null +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -0,0 +1,27 @@ +import { Config, ConfigProvider, Context, Effect, Layer } from "effect" +import { ConfigService } from "@/effect/config-service" + +export class Service extends ConfigService.Service()("@opencode/RuntimeFlags", { + pure: Config.boolean("OPENCODE_PURE").pipe(Config.withDefault(false)), + disableDefaultPlugins: Config.boolean("OPENCODE_DISABLE_DEFAULT_PLUGINS").pipe(Config.withDefault(false)), +}) {} + +export type Info = Context.Service.Shape + +const emptyConfigLayer = Service.defaultLayer.pipe( + Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({}))), + Layer.orDie, +) + +export const layer = (overrides: Partial = {}) => + Layer.effect( + Service, + Effect.gen(function* () { + const flags = yield* Service + return Service.of({ ...flags, ...overrides }) + }), + ).pipe(Layer.provide(emptyConfigLayer)) + +export const defaultLayer = Service.defaultLayer.pipe(Layer.orDie) + +export * as RuntimeFlags from "./runtime-flags" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 68d47916cc..e87f6db238 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -9,7 +9,6 @@ import { Config } from "@/config/config" import { Bus } from "../bus" import * as Log from "@opencode-ai/core/util/log" import { createOpencodeClient } from "@opencode-ai/sdk" -import { Flag } from "@opencode-ai/core/flag/flag" import { ServerAuth } from "@/server/auth" import { CodexAuthPlugin } from "./codex" import { Session } from "@/session/session" @@ -28,6 +27,7 @@ import { PluginLoader } from "./loader" import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared" import { registerAdapter } from "@/control-plane/adapters" import type { WorkspaceAdapter } from "@/control-plane/types" +import { RuntimeFlags } from "@/effect/runtime-flags" const log = Log.create({ service: "plugin" }) @@ -112,6 +112,7 @@ export const layer = Layer.effect( Effect.gen(function* () { const bus = yield* Bus.Service const config = yield* Config.Service + const flags = yield* RuntimeFlags.Service const state = yield* InstanceState.make( Effect.fn("Plugin.state")(function* (ctx) { @@ -148,7 +149,7 @@ export const layer = Layer.effect( $: typeof Bun === "undefined" ? undefined : Bun.$, } - for (const plugin of INTERNAL_PLUGINS) { + for (const plugin of flags.disableDefaultPlugins ? [] : INTERNAL_PLUGINS) { log.info("loading internal plugin", { name: plugin.name }) const init = yield* Effect.tryPromise({ try: () => plugin(input), @@ -159,8 +160,8 @@ export const layer = Layer.effect( if (init._tag === "Some") hooks.push(init.value) } - const plugins = Flag.OPENCODE_PURE ? [] : (cfg.plugin_origins ?? []) - if (Flag.OPENCODE_PURE && cfg.plugin_origins?.length) { + const plugins = flags.pure ? [] : (cfg.plugin_origins ?? []) + if (flags.pure && cfg.plugin_origins?.length) { log.info("skipping external plugins in pure mode", { count: cfg.plugin_origins.length }) } if (plugins.length) yield* config.waitForDependencies() @@ -285,6 +286,10 @@ export const layer = Layer.effect( }), ) -export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Config.defaultLayer)) +export const defaultLayer = layer.pipe( + Layer.provide(Bus.layer), + Layer.provide(Config.defaultLayer), + Layer.provide(RuntimeFlags.defaultLayer), +) export * as Plugin from "." diff --git a/packages/opencode/test/agent/plugin-agent-regression.test.ts b/packages/opencode/test/agent/plugin-agent-regression.test.ts index e2dd8a5f7c..60d59ee907 100644 --- a/packages/opencode/test/agent/plugin-agent-regression.test.ts +++ b/packages/opencode/test/agent/plugin-agent-regression.test.ts @@ -7,6 +7,7 @@ import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Config } from "../../src/config/config" import { Env } from "../../src/env" +import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin" import { AccountTest } from "../fake/account" import { AuthTest } from "../fake/auth" @@ -29,7 +30,11 @@ const configLayer = Config.layer.pipe( Layer.provide(AccountTest.empty), Layer.provide(NpmTest.noop), ) -const pluginLayer = Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer)) +const pluginLayer = Plugin.layer.pipe( + Layer.provide(Bus.layer), + Layer.provide(configLayer), + Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), +) const agentLayer = Agent.layer.pipe( Layer.provide(configLayer), Layer.provide(AuthTest.empty), diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts new file mode 100644 index 0000000000..5c9518a271 --- /dev/null +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -0,0 +1,55 @@ +import { describe, expect } from "bun:test" +import { ConfigProvider, Effect, Layer } from "effect" +import { RuntimeFlags } from "../../src/effect/runtime-flags" +import { it } from "../lib/effect" + +const fromConfig = (input: Record) => + RuntimeFlags.defaultLayer.pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input)))) + +const readFlags = RuntimeFlags.Service.useSync((flags) => flags) + +describe("RuntimeFlags", () => { + it.effect("defaultLayer parses plugin flags from the active ConfigProvider", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe( + Effect.provide( + fromConfig({ + OPENCODE_PURE: "true", + OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + }), + ), + ) + + expect(flags.pure).toBe(true) + expect(flags.disableDefaultPlugins).toBe(true) + }), + ) + + it.effect("layer accepts partial test overrides and fills defaults from Config definitions", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe(Effect.provide(RuntimeFlags.layer({ disableDefaultPlugins: true }))) + + expect(flags.pure).toBe(false) + expect(flags.disableDefaultPlugins).toBe(true) + }), + ) + + it.effect("layer ignores the active ConfigProvider for omitted test overrides", () => + Effect.gen(function* () { + const flags = yield* readFlags.pipe( + Effect.provide(RuntimeFlags.layer()), + Effect.provide( + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + OPENCODE_PURE: "true", + OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + }), + ), + ), + ) + + expect(flags.pure).toBe(false) + expect(flags.disableDefaultPlugins).toBe(false) + }), + ) +}) diff --git a/packages/opencode/test/plugin/auth-override.test.ts b/packages/opencode/test/plugin/auth-override.test.ts index 402d755da7..adc66e48c5 100644 --- a/packages/opencode/test/plugin/auth-override.test.ts +++ b/packages/opencode/test/plugin/auth-override.test.ts @@ -7,6 +7,7 @@ import { provideInstance, TestInstance, tmpdirScoped } from "../fixture/fixture" import { ProviderAuth } from "@/provider/auth" import { ProviderID } from "../../src/provider/schema" import { Plugin } from "@/plugin" +import { RuntimeFlags } from "@/effect/runtime-flags" import { Auth } from "@/auth" import { Bus } from "@/bus" import { TestConfig } from "../fixture/config" @@ -21,6 +22,7 @@ function layer(directory: string, plugins: string[]) { Layer.provide( Plugin.layer.pipe( Layer.provide(Bus.layer), + Layer.provide(RuntimeFlags.layer()), Layer.provide( TestConfig.layer({ get: () => diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 1b6372390e..6b1dd306dc 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, describe, expect, spyOn } from "bun:test" +import { afterEach, describe, expect, spyOn } from "bun:test" import { Effect, Layer } from "effect" import fs from "fs/promises" import path from "path" @@ -8,23 +8,13 @@ import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/f import { testEffect } from "../lib/effect" import { Filesystem } from "@/util/filesystem" -const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS -process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" - const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Bus } = await import("../../src/bus") const { Npm } = await import("@opencode-ai/core/npm") const { TestConfig } = await import("../fixture/config") - -afterAll(() => { - if (disableDefault === undefined) { - delete process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS - return - } - process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = disableDefault -}) +const { RuntimeFlags } = await import("../../src/effect/runtime-flags") afterEach(async () => { await disposeAllInstances() @@ -43,7 +33,7 @@ function withTmp( }) } -function load(dir: string) { +function load(dir: string, flags?: Parameters[0]) { const source = path.join(dir, "opencode.json") return Effect.gen(function* () { const config = yield* Effect.promise( @@ -57,6 +47,7 @@ function load(dir: string) { Effect.provide( Plugin.layer.pipe( Layer.provide(Bus.layer), + Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, ...flags })), Layer.provide( TestConfig.layer({ get: () => @@ -934,25 +925,14 @@ export default { }, (tmp) => Effect.gen(function* () { - const pure = process.env.OPENCODE_PURE - process.env.OPENCODE_PURE = "1" - - try { - yield* load(tmp.path) - const called = yield* Effect.promise(() => - fs - .readFile(tmp.extra.mark, "utf8") - .then(() => true) - .catch(() => false), - ) - expect(called).toBe(false) - } finally { - if (pure === undefined) { - delete process.env.OPENCODE_PURE - } else { - process.env.OPENCODE_PURE = pure - } - } + yield* load(tmp.path, { pure: true }) + const called = yield* Effect.promise(() => + fs + .readFile(tmp.extra.mark, "utf8") + .then(() => true) + .catch(() => false), + ) + expect(called).toBe(false) }), ), ) diff --git a/packages/opencode/test/plugin/trigger.test.ts b/packages/opencode/test/plugin/trigger.test.ts index 18fe0e82ef..94642fba62 100644 --- a/packages/opencode/test/plugin/trigger.test.ts +++ b/packages/opencode/test/plugin/trigger.test.ts @@ -10,6 +10,7 @@ import { Auth } from "../../src/auth" import { Bus } from "../../src/bus" import { Config } from "../../src/config/config" import { Env } from "../../src/env" +import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Plugin } from "../../src/plugin/index" import { ModelID, ProviderID } from "../../src/provider/schema" import { provideTmpdirInstance } from "../fixture/fixture" @@ -33,7 +34,11 @@ const configLayer = Config.layer.pipe( ) const it = testEffect( Layer.mergeAll( - Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer)), + Plugin.layer.pipe( + Layer.provide(Bus.layer), + Layer.provide(configLayer), + Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), + ), CrossSpawnSpawner.defaultLayer, ), ) diff --git a/packages/opencode/test/plugin/workspace-adapter.test.ts b/packages/opencode/test/plugin/workspace-adapter.test.ts index d4aaae4a9d..bef8604324 100644 --- a/packages/opencode/test/plugin/workspace-adapter.test.ts +++ b/packages/opencode/test/plugin/workspace-adapter.test.ts @@ -11,6 +11,7 @@ import { Auth } from "../../src/auth" import { Bus } from "../../src/bus" import { Config } from "../../src/config/config" import { Env } from "../../src/env" +import { RuntimeFlags } from "../../src/effect/runtime-flags" import { Workspace } from "../../src/control-plane/workspace" import { Plugin } from "../../src/plugin/index" import { InstanceBootstrap } from "../../src/project/bootstrap-service" @@ -35,7 +36,11 @@ const configLayer = Config.layer.pipe( Layer.provide(emptyAccount), Layer.provide(NpmTest.noop), ) -const pluginLayer = Plugin.layer.pipe(Layer.provide(Bus.layer), Layer.provide(configLayer)) +const pluginLayer = Plugin.layer.pipe( + Layer.provide(Bus.layer), + Layer.provide(configLayer), + Layer.provide(RuntimeFlags.layer({ disableDefaultPlugins: true })), +) const noopBootstrapLayer = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) const workspaceLayer = Workspace.defaultLayer.pipe( Layer.provide(InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrapLayer))), diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index b408f7ef11..6447c2fe93 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -45,7 +45,6 @@ process.env["OPENCODE_TEST_HOME"] = testHome // Set test managed config directory to isolate tests from system managed settings const testManagedConfigDir = path.join(dir, "managed") process.env["OPENCODE_TEST_MANAGED_CONFIG_DIR"] = testManagedConfigDir -process.env["OPENCODE_DISABLE_DEFAULT_PLUGINS"] = "true" // Write the cache version file to prevent global/index.ts from clearing the cache const cacheDir = path.join(dir, "cache", "opencode")