fix(core): preserve SDK plugins across location eviction (#35725)
This commit is contained in:
parent
f488089179
commit
8deb0e5780
13 changed files with 209 additions and 105 deletions
|
|
@ -890,8 +890,14 @@ export interface VcsApi<E = never> {
|
||||||
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
|
||||||
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
|
||||||
|
|
||||||
|
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||||
|
export type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||||
|
export type Endpoint25_1Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location.evict"]>>
|
||||||
|
export type DebugEvictLocationOperation<E = never> = (input?: Endpoint25_1Input) => Effect.Effect<Endpoint25_1Output, E>
|
||||||
|
|
||||||
export interface DebugApi<E = never> {
|
export interface DebugApi<E = never> {
|
||||||
readonly location: DebugLocationOperation<E>
|
readonly location: DebugLocationOperation<E>
|
||||||
|
readonly evictLocation: DebugEvictLocationOperation<E>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppApi<E = never> {
|
export interface AppApi<E = never> {
|
||||||
|
|
|
||||||
|
|
@ -1069,7 +1069,15 @@ const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(r
|
||||||
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
|
||||||
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
|
type Endpoint25_1Request = Parameters<RawClient["server.debug"]["debug.location.evict"]>[0]
|
||||||
|
type Endpoint25_1Input = { readonly location?: Endpoint25_1Request["query"]["location"] }
|
||||||
|
const Endpoint25_1 = (raw: RawClient["server.debug"]) => (input?: Endpoint25_1Input) =>
|
||||||
|
raw["debug.location.evict"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
|
||||||
|
|
||||||
|
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({
|
||||||
|
location: Endpoint25_0(raw),
|
||||||
|
evictLocation: Endpoint25_1(raw),
|
||||||
|
})
|
||||||
|
|
||||||
const adaptClient = (raw: RawClient) => ({
|
const adaptClient = (raw: RawClient) => ({
|
||||||
health: adaptGroup0(raw["server.health"]),
|
health: adaptGroup0(raw["server.health"]),
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,8 @@ import type {
|
||||||
VcsDiffInput,
|
VcsDiffInput,
|
||||||
VcsDiffOutput,
|
VcsDiffOutput,
|
||||||
DebugLocationOutput,
|
DebugLocationOutput,
|
||||||
|
DebugEvictLocationInput,
|
||||||
|
DebugEvictLocationOutput,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { ClientError } from "./client-error"
|
import { ClientError } from "./client-error"
|
||||||
|
|
||||||
|
|
@ -1494,6 +1496,18 @@ export function make(options: ClientOptions) {
|
||||||
},
|
},
|
||||||
requestOptions,
|
requestOptions,
|
||||||
),
|
),
|
||||||
|
evictLocation: (input?: DebugEvictLocationInput, requestOptions?: RequestOptions) =>
|
||||||
|
request<DebugEvictLocationOutput>(
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: `/api/debug/location`,
|
||||||
|
query: { location: input?.["location"] },
|
||||||
|
successStatus: 204,
|
||||||
|
declaredStatuses: [401, 400],
|
||||||
|
empty: true,
|
||||||
|
},
|
||||||
|
requestOptions,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6130,3 +6130,11 @@ export type VcsDiffOutput = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>
|
||||||
|
|
||||||
|
export type DebugEvictLocationInput = {
|
||||||
|
readonly location?: {
|
||||||
|
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
|
||||||
|
}["location"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DebugEvictLocationOutput = void
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@ import { EventV2 } from "../event"
|
||||||
|
|
||||||
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
|
export const Updated = EventV2.ephemeral({ type: "sdk.plugin.updated", schema: {} })
|
||||||
|
|
||||||
export interface Store {
|
|
||||||
readonly plugins: Map<string, Plugin>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeStore = (): Store => ({ plugins: new Map() })
|
|
||||||
|
|
||||||
const defaultStore = makeStore()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
* Holds the plugins an embedder (the `@opencode-ai/sdk-next` host) contributes,
|
||||||
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
* so `PluginSupervisor` can add them on every Location boot through the ordinary
|
||||||
|
|
@ -22,10 +14,9 @@ const defaultStore = makeStore()
|
||||||
* config. Registration publishes an unlocated update so every booted Location
|
* config. Registration publishes an unlocated update so every booted Location
|
||||||
* reloads its plugin generation from the shared store.
|
* reloads its plugin generation from the shared store.
|
||||||
*
|
*
|
||||||
* The store is shared explicitly between the SDK construction graph and the
|
* Each host-global layer owns one private store. Location graphs reuse that
|
||||||
* embedded route graph because `LocationServiceMap` builds Location layers lazily
|
* layer through Effect's memoization, so separate hosts remain isolated while
|
||||||
* in a nested graph. Each embedded SDK creates its own store, so instances do not
|
* every Location in one host sees the same registrations.
|
||||||
* see each other's contributions.
|
|
||||||
*/
|
*/
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
readonly register: (plugin: Plugin) => Effect.Effect<void>
|
||||||
|
|
@ -34,26 +25,19 @@ export interface Interface {
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SdkPlugins") {}
|
||||||
|
|
||||||
export const layerWithStore = (store: Store) =>
|
export const layer = Layer.effect(
|
||||||
Layer.effect(
|
Service,
|
||||||
Service,
|
Effect.gen(function* () {
|
||||||
Effect.gen(function* () {
|
const events = yield* EventV2.Service
|
||||||
const events = yield* EventV2.Service
|
const plugins = new Map<string, Plugin>()
|
||||||
yield* Effect.addFinalizer(() =>
|
return Service.of({
|
||||||
|
register: (plugin) =>
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
store.plugins.clear()
|
plugins.set(plugin.id, plugin)
|
||||||
}),
|
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
|
||||||
)
|
all: () => [...plugins.values()],
|
||||||
return Service.of({
|
})
|
||||||
register: (plugin) =>
|
}),
|
||||||
Effect.sync(() => {
|
)
|
||||||
store.plugins.set(plugin.id, plugin)
|
|
||||||
}).pipe(Effect.andThen(events.publish(Updated, {})), Effect.asVoid),
|
|
||||||
all: () => [...store.plugins.values()],
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const layer = layerWithStore(defaultStore)
|
|
||||||
|
|
||||||
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
|
export const node = makeGlobalNode({ service: Service, layer, deps: [EventV2.node] })
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
import { LocationServiceMap } from "@opencode-ai/core/location-services"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||||
|
|
@ -28,8 +29,43 @@ import { Reference } from "../src/reference"
|
||||||
import { ToolRegistry } from "../src/tool/registry"
|
import { ToolRegistry } from "../src/tool/registry"
|
||||||
|
|
||||||
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
const it = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, LocationServiceMap.node])))
|
||||||
|
const itWithSdk = testEffect(
|
||||||
|
AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node, SdkPlugins.node, LocationServiceMap.node])),
|
||||||
|
)
|
||||||
|
|
||||||
describe("LocationServiceMap", () => {
|
describe("LocationServiceMap", () => {
|
||||||
|
itWithSdk.live("preserves embedded SDK plugins after Location eviction", () =>
|
||||||
|
Effect.acquireRelease(
|
||||||
|
Effect.promise(() => tmpdir()),
|
||||||
|
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
|
||||||
|
).pipe(
|
||||||
|
Effect.flatMap((dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const sdk = yield* SdkPlugins.Service
|
||||||
|
const locations = yield* LocationServiceMap.Service
|
||||||
|
const id = AgentV2.ID.make("persistent-sdk-agent")
|
||||||
|
const plugin = define({
|
||||||
|
id: "persistent-sdk-plugin",
|
||||||
|
effect: (ctx) => ctx.agent.transform((agents) => agents.update(id, () => {})),
|
||||||
|
})
|
||||||
|
yield* sdk.register(plugin)
|
||||||
|
|
||||||
|
const ref = Location.Ref.make({ directory: AbsolutePath.make(dir.path) })
|
||||||
|
const read = Effect.gen(function* () {
|
||||||
|
const supervisor = yield* PluginSupervisor.Service
|
||||||
|
yield* supervisor.ready
|
||||||
|
const agents = yield* AgentV2.Service
|
||||||
|
return yield* agents.get(id)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
|
||||||
|
yield* locations.invalidate(ref)
|
||||||
|
expect(yield* read.pipe(Effect.scoped, Effect.provide(locations.get(ref)))).toBeDefined()
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
it.live("applies ordered plugin config operations during boot", () =>
|
it.live("applies ordered plugin config operations during boot", () =>
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export const groupNames = {
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const endpointNames = {
|
export const endpointNames = {
|
||||||
|
"debug.location.evict": "evictLocation",
|
||||||
"session.messages": "list",
|
"session.messages": "list",
|
||||||
"integration.connect.key": "connectKey",
|
"integration.connect.key": "connectKey",
|
||||||
"integration.connect.oauth": "connectOauth",
|
"integration.connect.oauth": "connectOauth",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Location } from "@opencode-ai/schema/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||||
|
|
||||||
export const DebugGroup = HttpApiGroup.make("server.debug")
|
export const DebugGroup = HttpApiGroup.make("server.debug")
|
||||||
.add(
|
.add(
|
||||||
|
|
@ -14,4 +15,18 @@ export const DebugGroup = HttpApiGroup.make("server.debug")
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.delete("debug.location.evict", "/api/debug/location", {
|
||||||
|
query: LocationQuery,
|
||||||
|
success: HttpApiSchema.NoContent,
|
||||||
|
})
|
||||||
|
.annotateMerge(locationQueryOpenApi)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "v2.debug.location.evict",
|
||||||
|
summary: "Evict a loaded location",
|
||||||
|
description: "Dispose the requested location's cached services so its next use boots them fresh.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.annotateMerge(OpenApi.annotations({ title: "debug" }))
|
.annotateMerge(OpenApi.annotations({ title: "debug" }))
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,23 @@
|
||||||
import { OpenCode } from "@opencode-ai/client/effect"
|
import { OpenCode } from "@opencode-ai/client/effect"
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
|
||||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
|
||||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, ManagedRuntime } from "effect"
|
||||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { FetchHttpClient, HttpEffect, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
|
|
||||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
export const create = Effect.fn("OpenCode.create")(function* () {
|
||||||
const scope = yield* Scope.Scope
|
const runtime = yield* Effect.acquireRelease(
|
||||||
const memoMap = yield* Layer.makeMemoMap
|
Effect.sync(() => ManagedRuntime.make(createEmbeddedRoutes().pipe(Layer.provide(HttpServer.layerServices)))),
|
||||||
const sdkPlugins = SdkPlugins.makeStore()
|
(runtime) => runtime.disposeEffect,
|
||||||
const context = yield* Layer.buildWithMemoMap(
|
|
||||||
AppNodeBuilder.build(LayerNode.group([EventV2.node, PermissionSaved.node, Project.node, SdkPlugins.node]), [
|
|
||||||
[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)],
|
|
||||||
]),
|
|
||||||
memoMap,
|
|
||||||
scope,
|
|
||||||
)
|
)
|
||||||
|
const context = yield* runtime.contextEffect
|
||||||
const plugins = Context.get(context, SdkPlugins.Service)
|
const plugins = Context.get(context, SdkPlugins.Service)
|
||||||
const permissions = Context.get(context, PermissionSaved.Service)
|
const router = Context.get(context, HttpRouter.HttpRouter)
|
||||||
const project = Context.get(context, Project.Service)
|
const handler = HttpEffect.toWebHandler(router.asHttpEffect())
|
||||||
const web = yield* Effect.acquireRelease(
|
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => handler(new Request(input, init)), {
|
||||||
Effect.sync(() =>
|
|
||||||
HttpRouter.toWebHandler(
|
|
||||||
createEmbeddedRoutes(sdkPlugins).pipe(
|
|
||||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
|
||||||
HttpRouter.provideRequest(Layer.succeed(Project.Service, project)),
|
|
||||||
Layer.provide(HttpServer.layerServices),
|
|
||||||
),
|
|
||||||
{ disableLogger: true, memoMap },
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(web) => Effect.promise(web.dispose),
|
|
||||||
)
|
|
||||||
const fetch = Object.assign((input: RequestInfo | URL, init?: RequestInit) => web.handler(new Request(input, init)), {
|
|
||||||
preconnect: () => undefined,
|
preconnect: () => undefined,
|
||||||
}) satisfies typeof globalThis.fetch
|
}) satisfies typeof globalThis.fetch
|
||||||
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
|
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
|
||||||
Effect.provide(FetchHttpClient.layer),
|
Effect.provide(FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetch)), Layer.fresh)),
|
||||||
Effect.provideService(FetchHttpClient.Fetch, fetch),
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,50 @@ it.live(
|
||||||
25_000,
|
25_000,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"preserves SDK plugins across Location eviction",
|
||||||
|
() =>
|
||||||
|
withEmbedded("opencode-embedded-plugin-eviction-", (fixture) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||||
|
const ref = location(fixture)
|
||||||
|
const connected = yield* Latch.make(false)
|
||||||
|
const booted = yield* Deferred.make<void>()
|
||||||
|
// The rebooted Location commits its second plugin generation.
|
||||||
|
const recommitted = yield* Deferred.make<void>()
|
||||||
|
const generations = yield* Ref.make(0)
|
||||||
|
const id = `evicted-sdk-${crypto.randomUUID()}`
|
||||||
|
|
||||||
|
yield* opencode.events.subscribe().pipe(
|
||||||
|
Stream.runForEach((event) => {
|
||||||
|
if (event.type === "server.connected") return connected.open
|
||||||
|
if (event.type !== "plugin.updated" || event.location?.directory !== fixture.directory) return Effect.void
|
||||||
|
return Ref.updateAndGet(generations, (total) => total + 1).pipe(
|
||||||
|
Effect.flatMap((total) => {
|
||||||
|
if (total === 1) return Deferred.succeed(booted, undefined)
|
||||||
|
if (total === 2) return Deferred.succeed(recommitted, undefined)
|
||||||
|
return Effect.void
|
||||||
|
}),
|
||||||
|
Effect.asVoid,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Effect.forkScoped,
|
||||||
|
)
|
||||||
|
yield* connected.await
|
||||||
|
yield* opencode.plugin({ id, effect: () => Effect.void })
|
||||||
|
|
||||||
|
yield* opencode.plugin.list({ location: ref })
|
||||||
|
yield* Deferred.await(booted).pipe(Effect.timeout("5 seconds"))
|
||||||
|
yield* opencode.debug.evictLocation({ location: ref })
|
||||||
|
yield* opencode.plugin.list({ location: ref })
|
||||||
|
yield* Deferred.await(recommitted).pipe(Effect.timeout("5 seconds"))
|
||||||
|
|
||||||
|
expect((yield* opencode.plugin.list({ location: ref })).data.map((plugin) => String(plugin.id))).toContain(id)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
15_000,
|
||||||
|
)
|
||||||
|
|
||||||
it.live(
|
it.live(
|
||||||
"keeps SDK plugin registration isolated between embedded hosts",
|
"keeps SDK plugin registration isolated between embedded hosts",
|
||||||
() =>
|
() =>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,24 @@ import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||||
import { Effect, Option, RcMap } from "effect"
|
import { Effect, Option, RcMap } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
|
import { requestRef } from "../location"
|
||||||
|
|
||||||
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
|
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
|
||||||
handlers.handle(
|
handlers
|
||||||
"debug.location",
|
.handle(
|
||||||
Effect.fn(function* () {
|
"debug.location",
|
||||||
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
Effect.fn(function* () {
|
||||||
return Array.from(yield* RcMap.keys(locations.rcMap))
|
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
||||||
}),
|
return Array.from(yield* RcMap.keys(locations.rcMap))
|
||||||
),
|
}),
|
||||||
|
)
|
||||||
|
.handle(
|
||||||
|
"debug.location.evict",
|
||||||
|
Effect.fn(function* (ctx) {
|
||||||
|
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
|
||||||
|
// Resolve through requestRef so the key matches the shape the location
|
||||||
|
// middleware cached the services under.
|
||||||
|
yield* locations.invalidate(requestRef(ctx.request))
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
export * as ServerProcess from "./process"
|
export * as ServerProcess from "./process"
|
||||||
|
|
||||||
import { NodeHttpClient, NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpClient, NodeHttpServer } from "@effect/platform-node"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
|
||||||
import { Project } from "@opencode-ai/core/project"
|
|
||||||
import { HealthGroup } from "@opencode-ai/protocol/groups/health"
|
import { HealthGroup } from "@opencode-ai/protocol/groups/health"
|
||||||
import { Context, Effect, Layer, Option } from "effect"
|
import { Context, Effect, Layer, Option } from "effect"
|
||||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
|
|
@ -53,9 +48,11 @@ function listen(options: Options) {
|
||||||
function bind(hostname: string, port: number, password: string) {
|
function bind(hostname: string, port: number, password: string) {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true }).pipe(
|
createRoutes(password).pipe(
|
||||||
|
Layer.flatMap((context) =>
|
||||||
|
HttpServer.serve(Context.get(context, HttpRouter.HttpRouter).asHttpEffect(), HttpMiddleware.logger),
|
||||||
|
),
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
||||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(
|
||||||
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
Effect.tap(() => Effect.addFinalizer(() => Effect.sync(() => server.closeAllConnections()))),
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||||
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Effect, Layer, Option } from "effect"
|
import { Context, Effect, Layer, Option } from "effect"
|
||||||
import { Api } from "./api"
|
import { Api } from "./api"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
import { handlers } from "./handlers"
|
import { handlers } from "./handlers"
|
||||||
|
|
@ -40,6 +40,7 @@ const applicationServices = LayerNode.group([
|
||||||
Project.node,
|
Project.node,
|
||||||
SessionV2.node,
|
SessionV2.node,
|
||||||
PluginRuntime.providerNode,
|
PluginRuntime.providerNode,
|
||||||
|
SdkPlugins.node,
|
||||||
PermissionSaved.node,
|
PermissionSaved.node,
|
||||||
PtyTicket.node,
|
PtyTicket.node,
|
||||||
Credential.node,
|
Credential.node,
|
||||||
|
|
@ -55,26 +56,22 @@ export function createRoutes(password?: string) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) {
|
export function createEmbeddedRoutes() {
|
||||||
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), sdkPlugins)
|
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeRoutes<AuthError, AuthServices>(
|
function makeRoutes<AuthError, AuthServices>(auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>) {
|
||||||
auth: Layer.Layer<ServerAuth.Config, AuthError, AuthServices>,
|
|
||||||
sdkPlugins?: SdkPlugins.Store,
|
|
||||||
) {
|
|
||||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||||
const replacements: LayerNode.Replacements = [
|
const replacements: LayerNode.Replacements = [
|
||||||
[SessionExecution.node, SessionExecutionLocal.node],
|
[SessionExecution.node, SessionExecutionLocal.node],
|
||||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||||
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
||||||
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
|
|
||||||
]
|
]
|
||||||
const serviceLayer = simulateEnabled()
|
const serviceLayer = simulateEnabled()
|
||||||
? Layer.unwrap(
|
? Layer.unwrap(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { simulationReplacements, startDriveServer } = yield* Effect.promise(() =>
|
const { simulationReplacements, startDriveServer } = yield* Effect.promise(
|
||||||
import("@opencode-ai/simulation/backend"),
|
() => import("@opencode-ai/simulation/backend"),
|
||||||
)
|
)
|
||||||
if (driveEnabled()) startDriveServer()
|
if (driveEnabled()) startDriveServer()
|
||||||
return AppNodeBuilder.build(applicationServices, [
|
return AppNodeBuilder.build(applicationServices, [
|
||||||
|
|
@ -85,16 +82,24 @@ function makeRoutes<AuthError, AuthServices>(
|
||||||
)
|
)
|
||||||
: AppNodeBuilder.build(applicationServices, replacements)
|
: AppNodeBuilder.build(applicationServices, replacements)
|
||||||
|
|
||||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
return serviceLayer.pipe(
|
||||||
Layer.provide(handlers.pipe(Layer.provide(serviceLayer))),
|
Layer.flatMap((context) => {
|
||||||
Layer.provide(formLocationLayer),
|
const services = Layer.succeedContext(context)
|
||||||
Layer.provide(sessionLocationLayer),
|
const requestServices = Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service)(context))
|
||||||
Layer.provide(layer),
|
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||||
Layer.provide(authorizationLayer),
|
Layer.provide(handlers.pipe(Layer.provide(services))),
|
||||||
Layer.provide(schemaErrorLayer),
|
Layer.provide(formLocationLayer),
|
||||||
Layer.provide(auth),
|
Layer.provide(sessionLocationLayer),
|
||||||
Layer.provide(serviceLayer),
|
Layer.provide(layer),
|
||||||
Layer.provide(Observability.layer),
|
Layer.provide(authorizationLayer),
|
||||||
|
Layer.provide(schemaErrorLayer),
|
||||||
|
Layer.provide(auth),
|
||||||
|
Layer.provide(Observability.layer),
|
||||||
|
HttpRouter.provideRequest(requestServices),
|
||||||
|
Layer.provideMerge(services),
|
||||||
|
Layer.provideMerge(HttpRouter.layer),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,5 +113,4 @@ function driveEnabled() {
|
||||||
|
|
||||||
export const routes = createRoutes()
|
export const routes = createRoutes()
|
||||||
|
|
||||||
export const webHandler = () =>
|
export const webHandler = () => HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
|
||||||
HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)))
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue