feat(plugin): expose app metadata (#38179)
This commit is contained in:
parent
2ed8fe5960
commit
8de40be6ea
71 changed files with 477 additions and 286 deletions
|
|
@ -1,15 +1,18 @@
|
|||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { ServerInfo } from "../server-info"
|
||||
|
||||
export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handlers) =>
|
||||
handlers
|
||||
.handle("health.get", () =>
|
||||
Effect.succeed({
|
||||
healthy: true as const,
|
||||
version: InstallationVersion,
|
||||
pid: process.pid,
|
||||
Effect.gen(function* () {
|
||||
const info = yield* ServerInfo.Service
|
||||
return {
|
||||
healthy: true as const,
|
||||
version: info.app.version ?? "unknown",
|
||||
pid: process.pid,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.handle("health.stop", () => Effect.succeed({ accepted: false })),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import { Observability } from "@opencode-ai/util/observability"
|
|||
import { Schema } from "effect"
|
||||
|
||||
export const ServerOptions = Schema.Struct({
|
||||
client: Schema.optional(Schema.String),
|
||||
app: Schema.optional(
|
||||
Schema.Struct({
|
||||
name: Schema.optional(Schema.String),
|
||||
version: Schema.optional(Schema.String),
|
||||
channel: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
hostname: Schema.optional(Schema.String),
|
||||
port: Schema.optional(
|
||||
Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(65_535)),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export * as ServerProcess from "./process"
|
||||
|
||||
import { NodeHttpServer, NodeHttpServerRequest } from "@effect/platform-node"
|
||||
import { InstallationVersion } from "@opencode-ai/util/installation/version"
|
||||
import { SessionRestart } from "@opencode-ai/core/session/execution/restart"
|
||||
import { ServiceStatus } from "@opencode-ai/protocol/groups/health"
|
||||
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
||||
|
|
@ -50,7 +49,7 @@ export const start = Effect.fn("ServerProcess.start")(function* <E, R>(
|
|||
// Request fibers may continue inbound trace context, but must not inherit the server startup parent.
|
||||
yield* bound.http
|
||||
.serve(
|
||||
dispatch(password, status, application, shutdown).pipe(
|
||||
dispatch(password, status, application, shutdown, options.app?.version ?? "unknown").pipe(
|
||||
HttpMiddleware.cors({ allowedOrigins: isAllowedCorsOrigin, maxAge: 86_400 }),
|
||||
),
|
||||
HttpMiddleware.logger,
|
||||
|
|
@ -153,6 +152,7 @@ function dispatch(
|
|||
status: Status.Interface,
|
||||
application: Ref.Ref<Option.Option<App>>,
|
||||
shutdown: Deferred.Deferred<void>,
|
||||
version: string,
|
||||
): App {
|
||||
const auth = ServerAuth.Config.of({ password: Option.some(password), username: "opencode" })
|
||||
return Effect.gen(function* () {
|
||||
|
|
@ -166,7 +166,7 @@ function dispatch(
|
|||
: undefined
|
||||
if (lifecycle !== undefined) {
|
||||
if (!(yield* authorizedRequest(request, auth))) return unauthorized()
|
||||
return yield* control(request, lifecycle, status, () => Deferred.doneUnsafe(shutdown, Effect.void))
|
||||
return yield* control(request, lifecycle, status, () => Deferred.doneUnsafe(shutdown, Effect.void), version)
|
||||
}
|
||||
const state = yield* status.current
|
||||
const app = yield* Ref.get(application)
|
||||
|
|
@ -189,8 +189,9 @@ const control = Effect.fnUntraced(function* (
|
|||
route: "health" | "stop",
|
||||
status: Status.Interface,
|
||||
stop: () => void,
|
||||
version: string,
|
||||
) {
|
||||
if (route === "health") return yield* healthResponse(status)
|
||||
if (route === "health") return yield* healthResponse(status, version)
|
||||
const body = yield* request.json.pipe(Effect.option)
|
||||
const input = Option.isSome(body) ? Schema.decodeUnknownOption(ServiceStatus.StopRequest)(body.value) : Option.none()
|
||||
if (Option.isNone(input)) return HttpServerResponse.jsonUnsafe({ code: "invalid_request" }, { status: 400 })
|
||||
|
|
@ -210,10 +211,10 @@ const control = Effect.fnUntraced(function* (
|
|||
return HttpServerResponse.jsonUnsafe({ accepted })
|
||||
})
|
||||
|
||||
const healthResponse = Effect.fnUntraced(function* (status: Status.Interface) {
|
||||
const healthResponse = Effect.fnUntraced(function* (status: Status.Interface, version: string) {
|
||||
const state = yield* status.current
|
||||
return HttpServerResponse.jsonUnsafe(
|
||||
{ healthy: true, version: InstallationVersion, pid: process.pid },
|
||||
{ healthy: true, version, pid: process.pid },
|
||||
{
|
||||
status: state.type === "ready" ? 200 : state.type === "failed" ? 500 : 503,
|
||||
headers: state.type === "starting" || state.type === "stopping" ? { "retry-after": "1" } : undefined,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { App } from "@opencode-ai/core/app"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { httpClient } from "@opencode-ai/util/effect/app-node-platform"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
|
|
@ -6,7 +7,6 @@ import { EventV2 } from "@opencode-ai/core/event"
|
|||
import { EventLogger } from "@opencode-ai/core/event-logger"
|
||||
import { FileSystemSearch } from "@opencode-ai/core/filesystem/search"
|
||||
import { Observability } from "@opencode-ai/util/observability"
|
||||
import { Client } from "@opencode-ai/util/client"
|
||||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
|
|
@ -15,12 +15,9 @@ import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
|||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionCompaction } from "@opencode-ai/core/session/compaction"
|
||||
import { SessionGenerateNode } from "@opencode-ai/core/session/generate-node"
|
||||
import { SessionModelRequest } from "@opencode-ai/core/session/model-request"
|
||||
import { SessionTitle } from "@opencode-ai/core/session/title"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
import { Job } from "@opencode-ai/core/job"
|
||||
import { MCP } from "@opencode-ai/core/mcp/index"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { InstructionDiscovery } from "@opencode-ai/core/instruction-discovery"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
|
||||
|
|
@ -88,7 +85,7 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
const pluginRuntimeCell = PluginRuntime.makeCell()
|
||||
const replacements: LayerNode.Replacements = [
|
||||
[Database.node, Database.configured(options.database)],
|
||||
[Client.node, Client.configured(options.client)],
|
||||
[App.node, App.configured(options.app)],
|
||||
[ModelsDev.node, ModelsDev.configured(options.models)],
|
||||
[Watcher.node, Watcher.configured({ enabled: options.fs?.filewatcher })],
|
||||
[FileSystemSearch.node, FileSystemSearch.configured({ fff: options.fs?.fff })],
|
||||
|
|
@ -105,6 +102,15 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
[CommandV2.node, CommandV2.configured({ gitbash: options.windows?.gitbash })],
|
||||
[Pty.node, Pty.configured({ gitbash: options.windows?.gitbash })],
|
||||
[Shell.node, Shell.configured({ gitbash: options.windows?.gitbash })],
|
||||
[
|
||||
MCP.node,
|
||||
MCP.configured({
|
||||
clientInfo: {
|
||||
name: options.app?.name ?? "opencode",
|
||||
version: options.app?.version ?? "unknown",
|
||||
},
|
||||
}),
|
||||
],
|
||||
[PluginRuntime.node, PluginRuntime.layerWithCell(pluginRuntimeCell)],
|
||||
[PluginRuntime.providerNode, PluginRuntime.providerNodeWithCell(pluginRuntimeCell)],
|
||||
]
|
||||
|
|
@ -112,7 +118,7 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
? Layer.unwrap(
|
||||
Effect.gen(function* () {
|
||||
const { simulationReplacements } = yield* Effect.promise(() => import("@opencode-ai/simulation/backend"))
|
||||
const simulation = yield* simulationReplacements()
|
||||
const simulation = yield* simulationReplacements({ version: App.make(options.app).version })
|
||||
return AppNodeBuilder.build(applicationServices, [...replacements, ...simulation])
|
||||
}),
|
||||
)
|
||||
|
|
@ -123,7 +129,7 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
const services = Layer.succeedContext(context)
|
||||
const requestServices = Layer.merge(
|
||||
Layer.succeedContext(Context.pick(PermissionSaved.Service, Project.Service, WellKnown.Service)(context)),
|
||||
ServerInfo.layer(serviceURLs),
|
||||
ServerInfo.layer(serviceURLs, options.app),
|
||||
)
|
||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||
Layer.provide(handlers.pipe(Layer.provide(services))),
|
||||
|
|
@ -133,7 +139,14 @@ function makeRoutes<AuthError, AuthServices>(
|
|||
Layer.provide(authorizationLayer),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(auth),
|
||||
Layer.provide(Observability.layer(options.observability).pipe(Layer.provide(Client.layer(options.client)))),
|
||||
Layer.provide(
|
||||
Observability.layer({
|
||||
...options.observability,
|
||||
client: options.app?.name,
|
||||
version: options.app?.version,
|
||||
channel: options.app?.channel,
|
||||
}),
|
||||
),
|
||||
HttpRouter.provideRequest(requestServices),
|
||||
Layer.provideMerge(services),
|
||||
Layer.provideMerge(HttpRouter.layer),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Context, Layer } from "effect"
|
||||
import { networkInterfaces } from "node:os"
|
||||
import type { ServerOptions } from "./options"
|
||||
|
||||
export class Service extends Context.Service<Service, { readonly urls: () => ReadonlyArray<string> }>()(
|
||||
"@opencode-ai/server/ServerInfo",
|
||||
) {}
|
||||
export class Service extends Context.Service<
|
||||
Service,
|
||||
{ readonly urls: () => ReadonlyArray<string>; readonly app: NonNullable<ServerOptions["app"]> }
|
||||
>()("@opencode-ai/server/ServerInfo") {}
|
||||
|
||||
export function layer(urls: () => ReadonlyArray<string>) {
|
||||
return Layer.succeed(Service, Service.of({ urls }))
|
||||
export function layer(urls: () => ReadonlyArray<string>, app: ServerOptions["app"] = {}) {
|
||||
return Layer.succeed(Service, Service.of({ urls, app }))
|
||||
}
|
||||
|
||||
export function connectionURLs(value: string, requestedHostname?: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue