refactor(core): separate Code Mode host wiring
This commit is contained in:
parent
e113aad5e0
commit
42b63d6660
8 changed files with 190 additions and 93 deletions
1
bun.lock
1
bun.lock
|
|
@ -813,6 +813,7 @@
|
||||||
"version": "1.17.14",
|
"version": "1.17.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
|
"@opencode-ai/codemode": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/simulation": "workspace:*",
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -50,13 +50,13 @@ export interface CodeModeTools {
|
||||||
export const create = (options: {
|
export const create = (options: {
|
||||||
readonly registrations: ReadonlyMap<string, Registration>
|
readonly registrations: ReadonlyMap<string, Registration>
|
||||||
readonly current: (name: string) => Registration | undefined
|
readonly current: (name: string) => Registration | undefined
|
||||||
readonly tools?: CodeModeTools
|
readonly tools: CodeModeTools
|
||||||
}) => {
|
}) => {
|
||||||
const runtime = (
|
const runtime = (
|
||||||
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
invoke: (name: string, registration: Registration, input: unknown) => Effect.Effect<unknown, unknown>,
|
||||||
hooks?: CodeMode.ToolCallHooks,
|
hooks?: CodeMode.ToolCallHooks,
|
||||||
) => {
|
) => {
|
||||||
const tools: CodeModeTools = Object.assign(Object.create(null), options.tools)
|
const tools = cloneTools(options.tools)
|
||||||
for (const [name, registration] of options.registrations) {
|
for (const [name, registration] of options.registrations) {
|
||||||
const child = definition(name, registration.tool)
|
const child = definition(name, registration.tool)
|
||||||
const value = Tool.make({
|
const value = Tool.make({
|
||||||
|
|
@ -168,6 +168,15 @@ export const create = (options: {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneTools(tools: CodeModeTools): CodeModeTools {
|
||||||
|
return Object.assign(
|
||||||
|
Object.create(null),
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(tools).map(([name, value]) => [name, Tool.isDefinition(value) ? value : cloneTools(value)]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
function displayInput(input: unknown): Record<string, unknown> | undefined {
|
||||||
if (input === null || input === undefined) return
|
if (input === null || input === undefined) return
|
||||||
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
if (typeof input !== "object" || Array.isArray(input)) return { input }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { definition, permission, registrationEntries, RegistrationError, settle,
|
||||||
import { Tools } from "./tools"
|
import { Tools } from "./tools"
|
||||||
import { ToolHooks } from "./hooks"
|
import { ToolHooks } from "./hooks"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
|
import { LayerNode } from "../effect/layer-node"
|
||||||
import { SessionError } from "@opencode-ai/schema/session-error"
|
import { SessionError } from "@opencode-ai/schema/session-error"
|
||||||
import { toSessionError } from "../session/to-session-error"
|
import { toSessionError } from "../session/to-session-error"
|
||||||
|
|
||||||
|
|
@ -52,10 +53,16 @@ export interface Settlement {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/ToolRegistry") {}
|
||||||
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools?: CodeModeTools }>()(
|
class CodeModeCatalog extends Context.Service<CodeModeCatalog, { readonly tools: CodeModeTools }>()(
|
||||||
"@opencode/v2/CodeModeCatalog",
|
"@opencode/v2/CodeModeCatalog",
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
const codeModeCatalogNode = makeLocationNode({
|
||||||
|
service: CodeModeCatalog,
|
||||||
|
layer: Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: {} })),
|
||||||
|
deps: [],
|
||||||
|
})
|
||||||
|
|
||||||
const registryLayer = Layer.effect(
|
const registryLayer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
@ -209,10 +216,9 @@ const registryLayer = Layer.effect(
|
||||||
}
|
}
|
||||||
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
const direct = new Map(Array.from(registrations).filter(([, registration]) => !registration.deferred))
|
||||||
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
|
const deferred = new Map(Array.from(registrations).filter(([, registration]) => registration.deferred))
|
||||||
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : undefined
|
const tools = Flag.CODEMODE_ENABLED ? codeModeTools : {}
|
||||||
const execute =
|
const execute =
|
||||||
(deferred.size > 0 || (tools !== undefined && Object.keys(tools).length > 0)) &&
|
(deferred.size > 0 || Object.keys(tools).length > 0) && !whollyDisabled("execute", input.permissions ?? [])
|
||||||
!whollyDisabled("execute", input.permissions ?? [])
|
|
||||||
? ExecuteTool.create({
|
? ExecuteTool.create({
|
||||||
registrations: deferred,
|
registrations: deferred,
|
||||||
current: (name) => local.get(name)?.at(-1)?.registration,
|
current: (name) => local.get(name)?.at(-1)?.registration,
|
||||||
|
|
@ -239,37 +245,28 @@ const registryLayer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const makeLayer = (codeModeTools?: CodeModeTools) => {
|
const layer = Layer.effect(
|
||||||
return Layer.effect(
|
Tools.Service,
|
||||||
Tools.Service,
|
Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
|
||||||
Service.use((registry) => Effect.succeed(Tools.Service.of({ register: registry.register }))),
|
).pipe(Layer.provideMerge(registryLayer))
|
||||||
).pipe(
|
|
||||||
Layer.provideMerge(registryLayer),
|
|
||||||
Layer.provide(Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools: codeModeTools }))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
function whollyDisabled(action: string, rules: PermissionV2.Ruleset) {
|
||||||
const rule = rules.findLast((rule) => Wildcard.match(action, rule.action))
|
const rule = rules.findLast((rule) => Wildcard.match(action, rule.action))
|
||||||
return rule?.resource === "*" && rule.effect === "deny"
|
return rule?.resource === "*" && rule.effect === "deny"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nodes(codeModeTools?: CodeModeTools) {
|
export function codeModeReplacement(tools: CodeModeTools): LayerNode.Replacement {
|
||||||
const layer = makeLayer(codeModeTools)
|
return [codeModeCatalogNode, Layer.succeed(CodeModeCatalog, CodeModeCatalog.of({ tools }))]
|
||||||
return {
|
|
||||||
node: makeLocationNode({
|
|
||||||
service: Service,
|
|
||||||
layer,
|
|
||||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
|
||||||
}),
|
|
||||||
toolsNode: makeLocationNode({
|
|
||||||
service: Tools.Service,
|
|
||||||
layer,
|
|
||||||
deps: [ToolOutputStore.node, ToolHooks.node],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaults = nodes()
|
export const node = makeLocationNode({
|
||||||
export const node = defaults.node
|
service: Service,
|
||||||
export const toolsNode = defaults.toolsNode
|
layer,
|
||||||
|
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const toolsNode = makeLocationNode({
|
||||||
|
service: Tools.Service,
|
||||||
|
layer,
|
||||||
|
deps: [ToolOutputStore.node, ToolHooks.node, codeModeCatalogNode],
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ const outputStore = Layer.mock(ToolOutputStore.Service, {
|
||||||
})
|
})
|
||||||
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
const registryLayer = AppNodeBuilder.build(ToolRegistry.node, [[ToolOutputStore.node, outputStore]])
|
||||||
const it = testEffect(registryLayer)
|
const it = testEffect(registryLayer)
|
||||||
const codeModeNodes = ToolRegistry.nodes({
|
const codeModeTools: ToolRegistry.CodeModeTools = {
|
||||||
opencode: {
|
opencode: {
|
||||||
v2: {
|
v2: {
|
||||||
health: {
|
health: {
|
||||||
|
|
@ -44,8 +44,13 @@ const codeModeNodes = ToolRegistry.nodes({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
const codeModeIt = testEffect(AppNodeBuilder.build(codeModeNodes.node, [[ToolOutputStore.node, outputStore]]))
|
const codeModeIt = testEffect(
|
||||||
|
AppNodeBuilder.build(ToolRegistry.node, [
|
||||||
|
[ToolOutputStore.node, outputStore],
|
||||||
|
ToolRegistry.codeModeReplacement(codeModeTools),
|
||||||
|
]),
|
||||||
|
)
|
||||||
const identity = {
|
const identity = {
|
||||||
agent: AgentV2.ID.make("build"),
|
agent: AgentV2.ID.make("build"),
|
||||||
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
assistantMessageID: SessionMessage.ID.make("msg_registry"),
|
||||||
|
|
@ -91,6 +96,28 @@ describe("ToolRegistry", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
codeModeIt.effect("keeps host Code Mode trees immutable while merging deferred tools", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const service = yield* ToolRegistry.Service
|
||||||
|
yield* service.register({ echo: make() }, { group: "opencode", deferred: true })
|
||||||
|
|
||||||
|
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||||
|
expect((yield* toolDefinitions(service))[0]?.description).toContain("tools.opencode.echo")
|
||||||
|
expect(
|
||||||
|
yield* executeTool(service, {
|
||||||
|
sessionID,
|
||||||
|
...identity,
|
||||||
|
call: {
|
||||||
|
type: "tool-call",
|
||||||
|
id: "call-opencode-echo",
|
||||||
|
name: "execute",
|
||||||
|
input: { code: 'return await tools.opencode.echo({ text: "hello" })' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({ type: "text", value: '{\n "text": "hello"\n}' })
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
|
it.effect("filters disabled tools with edit aliases and ordered wildcard precedence", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
|
|
|
||||||
66
packages/server/src/code-mode.ts
Normal file
66
packages/server/src/code-mode.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
export * as ServerCodeMode from "./code-mode"
|
||||||
|
|
||||||
|
import { NodeHttpClient } from "@effect/platform-node"
|
||||||
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
|
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||||
|
import { OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import type { Server } from "node:http"
|
||||||
|
import { Api } from "./api"
|
||||||
|
import { ServerAuth } from "./auth"
|
||||||
|
|
||||||
|
export function replacement(server: Server, password: string): LayerNode.Replacement {
|
||||||
|
return ToolRegistry.codeModeReplacement(makeTools(client(server), password))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
|
||||||
|
return {
|
||||||
|
opencode: bindTools(
|
||||||
|
OpenAPI.fromSpec({
|
||||||
|
spec: { ...OpenApi.fromApi(Api) },
|
||||||
|
baseUrl: "http://opencode.local",
|
||||||
|
headers: ServerAuth.headers({ username: "opencode", password }),
|
||||||
|
}).tools,
|
||||||
|
client,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function client(server: Server) {
|
||||||
|
return Layer.effect(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const client = yield* HttpClient.HttpClient
|
||||||
|
return HttpClient.mapRequest(client, (request) => {
|
||||||
|
const address = server.address()
|
||||||
|
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
|
||||||
|
const local =
|
||||||
|
address.address === "0.0.0.0" ? "127.0.0.1" : address.address === "::" ? "::1" : address.address
|
||||||
|
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
|
||||||
|
const url = new URL(request.url)
|
||||||
|
return HttpClientRequest.setUrl(
|
||||||
|
request,
|
||||||
|
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(tools).map(([name, value]) => [
|
||||||
|
name,
|
||||||
|
Tool.isDefinition<HttpClient.HttpClient>(value)
|
||||||
|
? Tool.make({
|
||||||
|
description: value.description,
|
||||||
|
input: value.input,
|
||||||
|
output: value.output,
|
||||||
|
run: (input) => value.run(input).pipe(Effect.provide(client)),
|
||||||
|
})
|
||||||
|
: bindTools(value, client),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/un
|
||||||
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"
|
||||||
|
import { ServerCodeMode } from "./code-mode"
|
||||||
import { createRoutes } from "./routes"
|
import { createRoutes } from "./routes"
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
|
|
@ -52,25 +53,10 @@ 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()
|
||||||
const codeModeClient = Layer.effect(
|
|
||||||
HttpClient.HttpClient,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const client = yield* HttpClient.HttpClient
|
|
||||||
return HttpClient.mapRequest(client, (request) => {
|
|
||||||
const address = server.address()
|
|
||||||
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
|
|
||||||
const local = hostname === "0.0.0.0" ? "127.0.0.1" : hostname === "::" ? "::1" : hostname
|
|
||||||
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
|
|
||||||
const url = new URL(request.url)
|
|
||||||
return HttpClientRequest.setUrl(
|
|
||||||
request,
|
|
||||||
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
|
|
||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password, codeModeClient), { disableListenLog: true }).pipe(
|
HttpRouter.serve(createRoutes(password, [ServerCodeMode.replacement(server, password)]), {
|
||||||
|
disableListenLog: true,
|
||||||
|
}).pipe(
|
||||||
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]))),
|
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,8 @@ import { SessionExecutionLocal } from "@opencode-ai/core/session/execution/local
|
||||||
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
import { PluginRuntime } from "@opencode-ai/core/plugin/runtime"
|
||||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
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 { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { HttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
|
||||||
import { HttpApiBuilder, OpenApi } from "effect/unstable/httpapi"
|
|
||||||
import { Effect, Layer, Option } from "effect"
|
import { Effect, Layer, Option } from "effect"
|
||||||
import { Api } from "./api"
|
import { Api } from "./api"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
|
|
@ -49,46 +47,36 @@ const applicationServices = LayerNode.group([
|
||||||
LocationServiceMap.node,
|
LocationServiceMap.node,
|
||||||
])
|
])
|
||||||
|
|
||||||
export function createRoutes(password?: string, codeModeClient?: Layer.Layer<HttpClient.HttpClient>) {
|
export function createRoutes(password?: string, replacements: LayerNode.Replacements = []) {
|
||||||
return makeRoutes(
|
return makeRoutes(
|
||||||
password
|
password
|
||||||
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) })
|
? ServerAuth.Config.configLayer({ username: "opencode", password: Option.some(password) })
|
||||||
: ServerAuth.Config.layer,
|
: ServerAuth.Config.layer,
|
||||||
undefined,
|
undefined,
|
||||||
codeModeClient
|
replacements,
|
||||||
? {
|
|
||||||
opencode: openCodeTools(
|
|
||||||
OpenAPI.fromSpec({
|
|
||||||
spec: { ...OpenApi.fromApi(Api) },
|
|
||||||
baseUrl: "http://opencode.local",
|
|
||||||
headers: ServerAuth.headers({ username: "opencode", password }),
|
|
||||||
}).tools,
|
|
||||||
codeModeClient,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store) {
|
export function createEmbeddedRoutes(sdkPlugins?: SdkPlugins.Store, replacements: LayerNode.Replacements = []) {
|
||||||
return makeRoutes(ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }), sdkPlugins)
|
return makeRoutes(
|
||||||
|
ServerAuth.Config.configLayer({ username: "opencode", password: Option.none() }),
|
||||||
|
sdkPlugins,
|
||||||
|
replacements,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
sdkPlugins?: SdkPlugins.Store,
|
||||||
codeModeTools?: ToolRegistry.CodeModeTools,
|
hostReplacements: LayerNode.Replacements = [],
|
||||||
) {
|
) {
|
||||||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||||
const codeMode = codeModeTools ? ToolRegistry.nodes(codeModeTools) : undefined
|
|
||||||
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] : []),
|
...(sdkPlugins ? [[SdkPlugins.node, SdkPlugins.layerWithStore(sdkPlugins)] as const] : []),
|
||||||
...(codeMode
|
...hostReplacements,
|
||||||
? [[ToolRegistry.node, codeMode.node] as const, [ToolRegistry.toolsNode, codeMode.toolsNode] as const]
|
|
||||||
: []),
|
|
||||||
]
|
]
|
||||||
const serviceLayer = simulateEnabled()
|
const serviceLayer = simulateEnabled()
|
||||||
? Layer.unwrap(
|
? Layer.unwrap(
|
||||||
|
|
@ -118,22 +106,6 @@ function makeRoutes<AuthError, AuthServices>(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCodeTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(tools).map(([name, value]) => [
|
|
||||||
name,
|
|
||||||
Tool.isDefinition<HttpClient.HttpClient>(value)
|
|
||||||
? Tool.make({
|
|
||||||
description: value.description,
|
|
||||||
input: value.input,
|
|
||||||
output: value.output,
|
|
||||||
run: (input) => value.run(input).pipe(Effect.provide(client)),
|
|
||||||
})
|
|
||||||
: openCodeTools(value, client),
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function simulateEnabled() {
|
function simulateEnabled() {
|
||||||
return !!process.env.OPENCODE_SIMULATE
|
return !!process.env.OPENCODE_SIMULATE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
packages/server/test/code-mode.test.ts
Normal file
39
packages/server/test/code-mode.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { expect, test } from "bun:test"
|
||||||
|
import { CodeMode } from "@opencode-ai/codemode"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
|
import { ServerCodeMode } from "../src/code-mode"
|
||||||
|
|
||||||
|
test("exposes the authenticated server API through CodeMode", async () => {
|
||||||
|
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
|
||||||
|
const client = Layer.succeed(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.make((request) => {
|
||||||
|
requests.push({ url: request.url, authorization: request.headers.authorization })
|
||||||
|
return Effect.succeed(
|
||||||
|
HttpClientResponse.fromWeb(
|
||||||
|
request,
|
||||||
|
Response.json(
|
||||||
|
{ healthy: true, version: "test", pid: 1 },
|
||||||
|
{ headers: { "content-type": "application/json" } },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const result = await CodeMode.make({ tools: ServerCodeMode.makeTools(client, "secret") })
|
||||||
|
.execute("return await tools.opencode.v2.health.get({})")
|
||||||
|
.pipe(Effect.runPromise)
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
ok: true,
|
||||||
|
value: { healthy: true, version: "test", pid: 1 },
|
||||||
|
toolCalls: [{ name: "opencode.v2.health.get" }],
|
||||||
|
})
|
||||||
|
expect(requests).toEqual([
|
||||||
|
{
|
||||||
|
url: "http://opencode.local/api/health",
|
||||||
|
authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue