import { expect } from "bun:test" import { App } from "@opencode-ai/core/app" import { Bus } from "@opencode-ai/core/bus" import { Database } from "@opencode-ai/core/database/database" import { llmClient } from "@opencode-ai/core/effect/app-node-platform" import { Watcher } from "@opencode-ai/core/filesystem/watcher" import { Form } from "@opencode-ai/core/form" import { Instance } from "@opencode-ai/core/instance" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import { ModelsDev } from "@opencode-ai/core/models-dev" import { Permission } from "@opencode-ai/core/permission" import { PluginRuntime } from "@opencode-ai/core/plugin/runtime" import { Session } from "@opencode-ai/core/session" import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model" import { Plugin } from "@opencode-ai/plugin/effect" import { Agent } from "@opencode-ai/schema/agent" import { Location } from "@opencode-ai/schema/location" import { AbsolutePath } from "@opencode-ai/schema/schema" import { LayerNode } from "@opencode-ai/util/effect/layer-node" import { Global } from "@opencode-ai/util/global" import { Context, Duration, Effect, Layer, LayerMap, Option, RcMap, Schema, Scope } from "effect" import { HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http" import { LanguageModel, LLMClient } from "../../ai/src" import { OpenAIChat } from "../../ai/src/protocols/openai-chat" import { TestLLM } from "../../ai/src/testing" import { tempGlobalLayer } from "../../core/test/fixture/global" import { tmpdirScoped } from "../../core/test/fixture/tmpdir" import { it } from "../../core/test/lib/effect" import { createEmbeddedRoutes } from "../src/routes" it.live( "isolates same-directory Session tools, hooks, commands, and HTTP requests through one selector", () => Effect.gen(function* () { const directory = yield* tmpdirScoped() const location = Location.Ref.make({ directory: AbsolutePath.make(directory.path) }) const first = { id: Session.ID.make("ses_instance_first"), tool: "instance_first", temperature: 0.1 } const second = { id: Session.ID.make("ses_instance_second"), tool: "instance_second", temperature: 0.2 } const configs = [first, second] const boots: Session.ID[] = [] const executed: Session.ID[] = [] const commands: Session.ID[] = [] const llm = yield* TestLLM.Test.pipe(Effect.provide(TestLLM.testLayer())) const model = SessionRunnerModel.resolved( LanguageModel.make({ id: "instance-model", provider: "test", route: OpenAIChat.route }), { capabilities: { tools: true, input: ["text"], output: ["text"] }, cost: [], limit: { context: 200_000, output: 8_192 }, }, ) const cell = PluginRuntime.makeCell() // Host and private instances must reuse the same global layer identities. const replacements: LayerNode.Replacements = [ Global.node.replace(tempGlobalLayer), Database.node.replace(Database.node), Bus.node.replace(Bus.node), App.node.replace(App.node), ModelsDev.node.replace(ModelsDev.configured({ fetch: false })), Watcher.node.replace(Watcher.configured({ enabled: false })), PluginRuntime.node.replace(PluginRuntime.layerWithCell(cell)), PluginRuntime.providerNode.replace(PluginRuntime.providerNodeWithCell(cell)), llmClient.replace(Layer.succeed(LLMClient.Service, llm)), SessionRunnerModel.node.replace( Layer.succeed(SessionRunnerModel.Service, { resolve: () => Effect.succeed(model) }), ), Instance.byLocationNode.replace( Layer.effect( Instance.Service, Effect.gen(function* () { const instances = yield* LayerMap.make( (id: Session.ID) => { const config = configs.find((config) => config.id === id) if (!config) throw new Error(`No instance configuration for ${id}`) return Instance.layer(location, { discovery: false, replacements, plugins: [ Plugin.define({ id: "session-instance", effect: Effect.fnUntraced(function* (ctx) { boots.push(config.id) yield* ctx.agent.transform((draft) => draft.update("build", (agent) => { agent.permissions = [{ action: "*", resource: "*", effect: "allow" }] }), ) yield* ctx.session.hook("prompt", (event) => Effect.sync(() => { event.prompt.text += ` [${config.tool}]` }), ) yield* ctx.session.hook("context", (event) => Effect.sync(() => { event.generation.temperature = config.temperature }), ) yield* ctx.permission.hook("evaluate", (event) => Effect.sync(() => { event.effect = event.action === "instance-test" ? "ask" : "allow" if (event.action === "instance-test") event.message = config.tool }), ) yield* ctx.tool.transform((draft) => draft.add({ name: config.tool, description: `Tool for ${config.id}`, input: Schema.Struct({}), output: Schema.String, options: { codemode: false }, execute: () => Effect.sync(() => { executed.push(config.id) return { output: config.id, content: config.id } }), }), ) yield* ctx.command.transform((draft) => draft.add({ name: "instance-check", execute: (input) => ctx.session .prompt({ sessionID: input.sessionID, text: `command ${config.tool}`, delivery: input.delivery, resume: false, }) .pipe( Effect.tap(() => Effect.sync(() => commands.push(config.id))), Effect.asVoid, ), }), ) }), }), ], }) }, { idleTimeToLive: Duration.infinity }, ) return Instance.Service.of({ provide: (session) => Effect.provide(instances.get(session.id)), provideIfLoaded: (session) => (effect) => Effect.scopedWith((scope) => Effect.gen(function* () { const context = yield* instances.contextEffectOption(session.id).pipe(Scope.provide(scope)) if (Option.isNone(context)) return Option.none() return Option.some(yield* effect.pipe(Effect.provide(context.value))) }), ), }) }), ), ), ] const context = yield* Layer.build( createEmbeddedRoutes({}, replacements).pipe(Layer.provide(HttpServer.layerServices)), ) const sessions = Context.get(context, Session.Service) const instances = Context.get(context, Instance.Service) const locations = Context.get(context, LocationServiceMap.Service) const handler = Context.get(context, HttpRouter.HttpRouter) .asHttpEffect() .pipe(HttpEffect.toWebHandlerWith(context)) const request = (route: string, body?: unknown) => Effect.promise(() => handler( new Request( `http://opencode.local${route}`, body === undefined ? undefined : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }, ), ), ) yield* Effect.forEach(configs, (config) => sessions.create({ id: config.id, title: config.tool, agent: Agent.ID.make("build"), model: model.ref, location, }), ) expect((yield* sessions.list()).data.map((session) => session.location)).toEqual([location, location]) expect(yield* sessions.messages({ sessionID: first.id })).toEqual([]) yield* sessions.get(second.id) for (const config of configs) { for (const resource of ["permission", "form"]) { const response = yield* request(`/api/session/${config.id}/${resource}`) expect(response.status).toBe(200) expect(yield* Effect.promise(() => response.json())).toEqual({ data: [] }) } } expect(boots).toEqual([]) for (const config of configs) { const admitted = yield* sessions.prompt({ sessionID: config.id, text: "run", resume: false }) expect(admitted.payload.text).toBe(`run [${config.tool}]`) } expect(boots).toEqual([first.id, second.id]) for (const config of configs) { yield* llm.push(TestLLM.tool(`call_${config.tool}`, config.tool, {}), TestLLM.text(config.id, config.tool)) yield* sessions.resume(config.id) } expect(executed).toEqual([first.id, second.id]) for (const config of configs) { yield* llm.push(TestLLM.text(`generated ${config.id}`, config.tool)) expect(yield* sessions.generate({ sessionID: config.id, prompt: "summarize" })).toBe(`generated ${config.id}`) yield* sessions.command({ sessionID: config.id, command: "instance-check", text: "" }) expect(yield* sessions.inbox(config.id)).toMatchObject([ { type: "user", payload: { text: `command ${config.tool} [${config.tool}]` } }, ]) } expect(commands).toEqual([first.id, second.id]) expect( (yield* llm.requests()).map((request) => ({ temperature: request.generation?.temperature, tools: request.tools?.filter((tool) => tool.name.startsWith("instance_")).map((tool) => tool.name), })), ).toEqual( [first, first, second, second, first, second].map((config) => ({ temperature: config.temperature, tools: [config.tool], })), ) // Seed through Core, then use HTTP to reach those exact private instances. const pending = yield* Effect.forEach(configs, (config) => Effect.gen(function* () { const session = yield* sessions.get(config.id) return yield* Effect.gen(function* () { const forms = yield* Form.Service const permissions = yield* Permission.Service const form = yield* forms.create({ sessionID: config.id, title: config.tool, fields: [{ key: "answer", type: "string" }], }) const permission = { id: Permission.ID.create(), sessionID: config.id, action: "instance-test", resources: [config.tool], } expect(yield* permissions.ask(permission)).toEqual({ id: permission.id, effect: "ask" }) const foreignID = config.id === first.id ? second.id : first.id const foreignForm = yield* forms.create({ sessionID: foreignID, title: "Foreign form", fields: [{ key: "answer", type: "string" }], }) const foreignPermission = yield* permissions.ask({ ...permission, id: Permission.ID.create(), sessionID: foreignID, }) return { session, form, permission: { ...permission, message: config.tool }, foreignForm, foreignPermission, } }).pipe(instances.provide(session)) }), ) for (const entry of pending) { const forms = yield* request(`/api/session/${entry.session.id}/form`) expect(forms.status).toBe(200) expect(yield* Effect.promise(() => forms.json())).toEqual({ data: [entry.form] }) const permissions = yield* request(`/api/session/${entry.session.id}/permission`) expect(permissions.status).toBe(200) expect(yield* Effect.promise(() => permissions.json())).toEqual({ data: [entry.permission] }) // These IDs exist in the selected instance, but belong to the other Session. expect((yield* request(`/api/session/${entry.session.id}/form/${entry.foreignForm.id}`)).status).toBe(404) expect( (yield* request(`/api/session/${entry.session.id}/permission/${entry.foreignPermission.id}`)).status, ).toBe(404) expect( (yield* request(`/api/session/${entry.session.id}/form/${entry.foreignForm.id}/reply`, { answer: { answer: "wrong" }, })).status, ).toBe(404) expect( (yield* request(`/api/session/${entry.session.id}/permission/${entry.foreignPermission.id}/reply`, { reply: "once", })).status, ).toBe(404) yield* Effect.gen(function* () { const forms = yield* Form.Service const permissions = yield* Permission.Service expect(yield* forms.state(entry.foreignForm.id)).toEqual({ status: "pending" }) expect(yield* permissions.get(entry.foreignPermission.id)).toMatchObject({ sessionID: entry.foreignForm.sessionID, }) }).pipe(instances.provide(entry.session)) } for (const entry of pending) { expect( (yield* request(`/api/session/${entry.session.id}/form/${entry.form.id}/reply`, { answer: { answer: entry.session.id }, })).status, ).toBe(204) expect( (yield* request(`/api/session/${entry.session.id}/permission/${entry.permission.id}/reply`, { reply: "once", })).status, ).toBe(204) yield* Effect.gen(function* () { const forms = yield* Form.Service const permissions = yield* Permission.Service expect(yield* forms.state(entry.form.id)).toEqual({ status: "answered", answer: { answer: entry.session.id }, }) expect(yield* permissions.get(entry.permission.id)).toBeUndefined() }).pipe(instances.provide(entry.session)) } expect(boots).toEqual([first.id, second.id]) expect(Array.from(yield* RcMap.keys(locations.rcMap))).toEqual([]) }), 15_000, )