diff --git a/packages/sdk-next/src/opencode.ts b/packages/sdk-next/src/opencode.ts
index 096b46d4b7..0c31592247 100644
--- a/packages/sdk-next/src/opencode.ts
+++ b/packages/sdk-next/src/opencode.ts
@@ -4,14 +4,30 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
-import { Context, Effect, Layer, Scope } from "effect"
+import { ConfigProvider, Context, Effect, Layer, Scope } from "effect"
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
-export const create = Effect.fn("OpenCode.create")(function* () {
+export interface Options {
+ /**
+ * Replaces the ConfigProvider this host's layers read while they are built,
+ * for example the OPENCODE_DB database placement. The default provider
+ * snapshots the process environment on first use, so per-host configuration
+ * must come through this seam rather than env mutation. Compose with
+ * ConfigProvider.orElse(ConfigProvider.fromEnv()) to keep environment
+ * fallback.
+ */
+ readonly configProvider?: ConfigProvider.ConfigProvider
+}
+
+export const create = Effect.fn("OpenCode.create")(function* (options?: Options) {
const scope = yield* Scope.Scope
const memoMap = yield* Layer.makeMemoMap
+ const withConfig = (layer: Layer.Layer) =>
+ options?.configProvider === undefined
+ ? layer
+ : layer.pipe(Layer.provide(ConfigProvider.layer(options.configProvider)))
const context = yield* Layer.buildWithMemoMap(
- AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, PermissionSaved.node])),
+ withConfig(AppNodeBuilder.build(LayerNode.group([ApplicationTools.node, PermissionSaved.node]))),
memoMap,
scope,
)
@@ -20,9 +36,11 @@ export const create = Effect.fn("OpenCode.create")(function* () {
const web = yield* Effect.acquireRelease(
Effect.sync(() =>
HttpRouter.toWebHandler(
- createEmbeddedRoutes().pipe(
- HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
- Layer.provide(HttpServer.layerServices),
+ withConfig(
+ createEmbeddedRoutes().pipe(
+ HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
+ Layer.provide(HttpServer.layerServices),
+ ),
),
{ disableLogger: true, memoMap },
),
@@ -47,3 +65,5 @@ export type Interface = Effect.Success>
export class Service extends Context.Service()("@opencode-ai/sdk-next/OpenCode") {}
export const layer = Layer.effect(Service, create())
+
+export const layerWith = (options: Options) => Layer.effect(Service, create(options))
diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts
index 01cbec0e66..773801fb40 100644
--- a/packages/sdk-next/test/embedded.test.ts
+++ b/packages/sdk-next/test/embedded.test.ts
@@ -1,17 +1,15 @@
-import { afterAll, expect, test } from "bun:test"
+import { expect, test } from "bun:test"
import { mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
-import { Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
+import { ConfigProvider, Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
import type { OpenCodeEvent } from "../src"
-// The database layer resolves OPENCODE_DB through Effect Config, and the
-// default ConfigProvider snapshots the process environment on first use, so
-// database placement is process-wide. Point every embedded host in this file
-// at one shared temporary database before anything builds a runtime.
-const databaseDirectory = await mkdtemp(join(tmpdir(), "opencode-embedded-db-"))
-process.env.OPENCODE_DB = join(databaseDirectory, "opencode.sqlite")
-afterAll(() => rm(databaseDirectory, { recursive: true, force: true }))
+// The default ConfigProvider snapshots the process environment on first use,
+// so per-host database placement must come through the create() config seam
+// rather than env mutation.
+const databaseConfig = (directory: string) =>
+ ConfigProvider.fromUnknown({ OPENCODE_DB: join(directory, "opencode.sqlite") })
test("embedded client uses the real router and handlers", async () => {
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
@@ -21,7 +19,7 @@ test("embedded client uses the real router and handlers", async () => {
try {
const program = Effect.gen(function* () {
- const opencode = yield* OpenCode.create()
+ const opencode = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
yield* opencode.tools.register({
embedded_tool: Tool.make({
description: "Embedded test tool",
@@ -103,6 +101,8 @@ test("embedded client uses the real router and handlers", async () => {
expect(missingMessage._tag).toBe("MessageNotFoundError")
})
await Effect.runPromise(Effect.scoped(program))
+ // The database materializes at the path this host's ConfigProvider chose.
+ expect(await Bun.file(join(directory, "opencode.sqlite")).exists()).toBe(true)
} finally {
await rm(directory, { recursive: true, force: true })
}
@@ -115,7 +115,7 @@ test("Location-owned runner events reach the ready global client", async () => {
try {
const program = Effect.gen(function* () {
- const opencode = yield* OpenCode.create()
+ const opencode = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
const connected = yield* Latch.make(false)
const prompted = yield* Deferred.make()
yield* opencode.events.subscribe().pipe(
@@ -151,8 +151,8 @@ test("independent embedded hosts do not share live notifications", async () => {
try {
const program = Effect.gen(function* () {
- const first = yield* OpenCode.create()
- const second = yield* OpenCode.create()
+ const first = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
+ const second = yield* OpenCode.create({ configProvider: databaseConfig(directory) })
const firstReady = yield* Latch.make(false)
const secondReady = yield* Latch.make(false)
const firstEvent = yield* Latch.make(false)
@@ -197,7 +197,7 @@ test("embedded client is available as a Layer service", async () => {
id: sessionID,
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
})
- }).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
+ }).pipe(Effect.provide(OpenCode.layerWith({ configProvider: databaseConfig(directory) })), Effect.scoped),
)
expect(created.id).toBe(sessionID)