Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
1093 lines
31 KiB
TypeScript
1093 lines
31 KiB
TypeScript
import { runtimePrivacyEnvironment } from '../shared/runtime-privacy-environment'
|
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
import fs, { mkdtemp, rm } from 'node:fs/promises'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { openCodeSubagentPluginSource } from './opencode-subagent-plugin'
|
|
import {
|
|
createServer,
|
|
type IncomingHttpHeaders,
|
|
type IncomingMessage,
|
|
type Server,
|
|
type ServerResponse
|
|
} from 'node:http'
|
|
import type { Socket } from 'node:net'
|
|
import { dirname, isAbsolute, resolve, join } from 'node:path'
|
|
import type {
|
|
ModelBridgeModelProtocol
|
|
} from '../shared/model-bridge-contracts'
|
|
import {
|
|
REMOTE_MODEL_GATEWAY_LIMITS,
|
|
remoteModelApiPathSchema,
|
|
remoteModelGatewayRequestSchema,
|
|
remoteModelGatewayResponseSchema,
|
|
type RemoteModelGatewayRequest
|
|
} from '../shared/remote-model-gateway-contracts'
|
|
import {
|
|
createUnixModelBridgeExchange,
|
|
ModelBridgeBrokerError,
|
|
type ModelBridgeExchange,
|
|
type ModelBridgeExchangeResult
|
|
} from './model-bridge-broker'
|
|
import {
|
|
boundedInteger,
|
|
closeServer,
|
|
raceWithAbort,
|
|
settleWithin
|
|
} from './async-utils'
|
|
|
|
export type ModelBridgeProtocol = ModelBridgeModelProtocol
|
|
|
|
// The Anthropic and OpenAI SDK adapters refuse to issue a request without an
|
|
// apiKey option. This fixed value is only a local compatibility marker. The
|
|
// loopback proxy verifies and removes its header before creating the wire
|
|
// request. A separate per-helper random route capability authenticates local
|
|
// requests, so this marker is neither a credential nor visible upstream.
|
|
export const MODEL_BRIDGE_SDK_AUTH_SENTINEL =
|
|
'goodbuddy-local-model-bridge-v1'
|
|
|
|
export type OpenCodeModelBridgeProviderConfig = {
|
|
model: string
|
|
snapshot: false
|
|
permission: 'ask' | 'allow'
|
|
agent: {
|
|
title: {
|
|
disable: true
|
|
}
|
|
build: {
|
|
permission: 'ask' | 'allow'
|
|
}
|
|
}
|
|
provider: Record<
|
|
string,
|
|
{
|
|
name: string
|
|
npm:
|
|
| '@ai-sdk/anthropic'
|
|
| '@ai-sdk/openai-compatible'
|
|
| '@ai-sdk/openai'
|
|
options: {
|
|
baseURL: string
|
|
apiKey?: typeof MODEL_BRIDGE_SDK_AUTH_SENTINEL
|
|
}
|
|
models: Record<
|
|
string,
|
|
{
|
|
name: string
|
|
attachment: boolean
|
|
modalities: {
|
|
input: Array<'text' | 'image'>
|
|
output: ['text']
|
|
}
|
|
provider: {
|
|
npm:
|
|
| '@ai-sdk/anthropic'
|
|
| '@ai-sdk/openai-compatible'
|
|
| '@ai-sdk/openai'
|
|
}
|
|
}
|
|
>
|
|
}
|
|
>
|
|
}
|
|
|
|
export type ModelBridgeHelperChild = {
|
|
kill?(signal?: NodeJS.Signals): unknown
|
|
once(event: 'error', listener: (error: Error) => void): unknown
|
|
once(
|
|
event: 'close',
|
|
listener: (code: number | null, signal: NodeJS.Signals | null) => void
|
|
): unknown
|
|
}
|
|
|
|
export type ModelBridgeHelperSpawn = (
|
|
executable: string,
|
|
args: readonly string[],
|
|
options: {
|
|
shell: false
|
|
stdio: 'inherit'
|
|
env: Readonly<NodeJS.ProcessEnv>
|
|
}
|
|
) => ModelBridgeHelperChild
|
|
|
|
const LOOPBACK_HOST = '127.0.0.1'
|
|
const DEFAULT_MAXIMUM_CONNECTIONS = 8
|
|
const DEFAULT_REQUEST_TIMEOUT_MS = 0
|
|
const RESPONSE_CLOSE_TIMEOUT_MS = 2_000
|
|
const MAXIMUM_HTTP_HEADER_BYTES = 16 * 1024
|
|
const MAXIMUM_HTTP_HEADER_COUNT = 32
|
|
const MODEL_BRIDGE_ROUTE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/u
|
|
const CREDENTIAL_INBOUND_HEADER_NAMES = new Set([
|
|
'api-key',
|
|
'authorization',
|
|
'cookie',
|
|
'proxy-authorization',
|
|
'set-cookie',
|
|
'x-api-key',
|
|
'x-goog-api-key'
|
|
])
|
|
const FORWARDED_HEADER_NAMES = [
|
|
'accept',
|
|
'content-type'
|
|
] as const
|
|
|
|
export class ModelBridgeLoopbackProxy {
|
|
readonly #exchange: ModelBridgeExchange
|
|
readonly #sharedSessions: boolean
|
|
readonly #sessionRoutes = new Map<string, { operationId: string; socketPath: string; imageToolName?: string }>()
|
|
readonly #routeToken: string
|
|
readonly #maximumConnections: number
|
|
readonly #requestTimeoutMs: number
|
|
readonly #sockets = new Set<Socket>()
|
|
readonly #dispatches = new Map<string, DispatchWaiter[]>()
|
|
#server?: Server
|
|
#origin?: string
|
|
#closing = false
|
|
#closePromise?: Promise<void>
|
|
|
|
constructor(options: {
|
|
exchange: ModelBridgeExchange
|
|
sharedSessions?: boolean
|
|
routeToken?: string
|
|
maximumConnections?: number
|
|
requestTimeoutMs?: number
|
|
}) {
|
|
this.#exchange = options.exchange
|
|
this.#sharedSessions = options.sharedSessions === true
|
|
this.#routeToken = parseModelBridgeRouteToken(
|
|
options.routeToken ??
|
|
randomBytes(32).toString('base64url')
|
|
)
|
|
this.#maximumConnections = boundedInteger(
|
|
options.maximumConnections ?? DEFAULT_MAXIMUM_CONNECTIONS,
|
|
1,
|
|
64,
|
|
'Model bridge loopback connection limit'
|
|
)
|
|
this.#requestTimeoutMs = boundedInteger(
|
|
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
0,
|
|
0x7fff_ffff,
|
|
'Model bridge loopback request timeout'
|
|
)
|
|
}
|
|
|
|
get origin(): string {
|
|
if (this.#origin === undefined) {
|
|
throw new Error('Model bridge loopback proxy is not listening')
|
|
}
|
|
return this.#origin
|
|
}
|
|
|
|
async listen(): Promise<string> {
|
|
if (this.#server !== undefined || this.#closing) {
|
|
throw new Error('Model bridge loopback proxy is already active')
|
|
}
|
|
const server = createServer(
|
|
{
|
|
maxHeaderSize: MAXIMUM_HTTP_HEADER_BYTES,
|
|
requireHostHeader: true
|
|
},
|
|
(request, response) => {
|
|
void this.#handleRequest(request, response)
|
|
}
|
|
)
|
|
server.maxConnections = this.#maximumConnections
|
|
server.maxHeadersCount = MAXIMUM_HTTP_HEADER_COUNT
|
|
server.maxRequestsPerSocket = 1
|
|
server.requestTimeout = this.#requestTimeoutMs
|
|
server.headersTimeout = Math.min(
|
|
this.#requestTimeoutMs,
|
|
30_000
|
|
)
|
|
server.keepAliveTimeout = 1_000
|
|
server.on('connection', (socket) => {
|
|
if (
|
|
this.#closing ||
|
|
this.#sockets.size >= this.#maximumConnections
|
|
) {
|
|
socket.destroy()
|
|
return
|
|
}
|
|
this.#sockets.add(socket)
|
|
socket.once('close', () => this.#sockets.delete(socket))
|
|
})
|
|
this.#server = server
|
|
try {
|
|
await listenOnLoopback(server)
|
|
const address = server.address()
|
|
if (address === null || typeof address === 'string') {
|
|
throw new Error(
|
|
'Model bridge loopback address is unavailable'
|
|
)
|
|
}
|
|
this.#origin =
|
|
`http://${LOOPBACK_HOST}:${address.port}/${this.#routeToken}`
|
|
return this.#origin
|
|
} catch (error) {
|
|
await this.close()
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.#closePromise !== undefined) {
|
|
await this.#closePromise
|
|
return
|
|
}
|
|
const closePromise = this.#close()
|
|
this.#closePromise = closePromise
|
|
try {
|
|
await closePromise
|
|
} finally {
|
|
if (this.#closePromise === closePromise) {
|
|
this.#closePromise = undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
async #close(): Promise<void> {
|
|
const server = this.#server
|
|
this.#server = undefined
|
|
this.#origin = undefined
|
|
this.#closing = true
|
|
try {
|
|
for (const socket of this.#sockets) {
|
|
socket.destroy()
|
|
}
|
|
await settleWithin(
|
|
server === undefined
|
|
? Promise.resolve()
|
|
: closeServer(server),
|
|
RESPONSE_CLOSE_TIMEOUT_MS
|
|
)
|
|
} finally {
|
|
this.#sockets.clear()
|
|
this.#sessionRoutes.clear()
|
|
this.#closing = false
|
|
}
|
|
}
|
|
|
|
async #handleRequest(
|
|
incoming: IncomingMessage,
|
|
outgoing: ServerResponse
|
|
): Promise<void> {
|
|
outgoing.setHeader('connection', 'close')
|
|
outgoing.setHeader('cache-control', 'no-store')
|
|
outgoing.setHeader('x-content-type-options', 'nosniff')
|
|
if (this.#closing) {
|
|
incoming.resume()
|
|
sendError(outgoing, 503, 'bridge-closing')
|
|
return
|
|
}
|
|
const cancellation = new AbortController()
|
|
let dispatched: ModelBridgeExchangeResult | undefined
|
|
let responseFlushed = false
|
|
let deliveryAcknowledged = false
|
|
let dispatchHeld = false
|
|
let dispatchKey = ''
|
|
let sessionId: string | undefined
|
|
let sessionRoute: { operationId: string; socketPath: string } | undefined
|
|
const onAborted = (): void => {
|
|
cancellation.abort(
|
|
new ModelBridgeBrokerError('request-cancelled')
|
|
)
|
|
}
|
|
const onResponseClose = (): void => {
|
|
if (!responseFlushed && !outgoing.writableFinished) {
|
|
onAborted()
|
|
void dispatched?.failDelivery(
|
|
new ModelBridgeBrokerError('transport-failed')
|
|
)
|
|
}
|
|
}
|
|
incoming.once('aborted', onAborted)
|
|
outgoing.once('close', onResponseClose)
|
|
const timeout = this.#requestTimeoutMs === 0 ? undefined : setTimeout(() => {
|
|
cancellation.abort(
|
|
new ModelBridgeBrokerError('request-timeout')
|
|
)
|
|
}, this.#requestTimeoutMs)
|
|
timeout?.unref?.()
|
|
|
|
try {
|
|
if (this.#sharedSessions && incoming.url?.startsWith(`/${this.#routeToken}/session`)) {
|
|
const url = new URL(incoming.url, 'http://localhost')
|
|
if (url.pathname !== `/${this.#routeToken}/session`) throw new HttpRequestError(404, 'path-not-allowed')
|
|
if (incoming.method === 'GET') {
|
|
const route = this.#sessionRoutes.get(url.searchParams.get('sessionId') ?? '')
|
|
if (!route) throw new HttpRequestError(409, 'session-inactive')
|
|
outgoing.setHeader('content-type', 'application/json')
|
|
outgoing.end(JSON.stringify({ operationId: route.operationId, imageToolName: route.imageToolName }))
|
|
return
|
|
}
|
|
if (incoming.method !== 'POST') throw new HttpRequestError(405, 'method-not-allowed')
|
|
const value = JSON.parse((await readBoundedBody(incoming)).toString('utf8')) as {
|
|
sessionId?: string; operationId?: string; socketPath?: string; release?: boolean; imageToolName?: string
|
|
}
|
|
if (
|
|
typeof value.sessionId !== 'string' || value.sessionId.length > 128 ||
|
|
typeof value.operationId !== 'string' || value.operationId.length > 128
|
|
) throw new HttpRequestError(400, 'request-invalid')
|
|
if (value.release) {
|
|
if (this.#sessionRoutes.get(value.sessionId)?.operationId === value.operationId) {
|
|
this.#sessionRoutes.delete(value.sessionId)
|
|
}
|
|
} else {
|
|
const socketPath = normalizedAbsolutePath(value.socketPath ?? '', 'Model bridge socket')
|
|
if (value.imageToolName !== undefined && !/^goodbuddy_image_[a-f0-9]{24}$/.test(value.imageToolName)) throw new HttpRequestError(400, 'request-invalid')
|
|
this.#sessionRoutes.set(value.sessionId, { operationId: value.operationId, socketPath, imageToolName: value.imageToolName })
|
|
}
|
|
outgoing.end('{}')
|
|
return
|
|
}
|
|
const request = await parseHttpRequest(
|
|
incoming,
|
|
this.#routeToken
|
|
)
|
|
let exchange = this.#exchange
|
|
if (this.#sharedSessions) {
|
|
const sessionHeader = incoming.headers['x-goodbuddy-session']
|
|
const operationId = incoming.headers['x-goodbuddy-operation']
|
|
sessionId = typeof sessionHeader === 'string' ? sessionHeader : undefined
|
|
const route = sessionId ? this.#sessionRoutes.get(sessionId) : undefined
|
|
if (!route || route.operationId !== operationId) throw new HttpRequestError(409, 'session-inactive')
|
|
sessionRoute = route
|
|
dispatchKey = `${sessionId}\0${operationId}`
|
|
exchange = createUnixModelBridgeExchange({ socketPath: route.socketPath })
|
|
}
|
|
await this.#acquireDispatch(dispatchKey, cancellation.signal)
|
|
dispatchHeld = true
|
|
if (sessionId && this.#sessionRoutes.get(sessionId) !== sessionRoute) {
|
|
throw new HttpRequestError(409, 'session-inactive')
|
|
}
|
|
dispatched = normalizeExchangeResult(
|
|
await raceWithAbort(exchange(request, {
|
|
requestId: `model-${randomUUID()}`,
|
|
signal: cancellation.signal
|
|
}), cancellation.signal)
|
|
)
|
|
const parsedResponse =
|
|
remoteModelGatewayResponseSchema.parse(dispatched.response)
|
|
if (!outgoing.headersSent && !outgoing.destroyed) {
|
|
outgoing.statusCode = parsedResponse.status
|
|
for (const [name, value] of Object.entries(
|
|
parsedResponse.headers
|
|
)) {
|
|
outgoing.setHeader(name, value)
|
|
}
|
|
const body = Buffer.from(
|
|
parsedResponse.bodyBase64,
|
|
'base64'
|
|
)
|
|
outgoing.setHeader('content-length', body.byteLength)
|
|
await endResponseFlushed(outgoing, body, () => {
|
|
responseFlushed = true
|
|
})
|
|
cancellation.signal.throwIfAborted()
|
|
await dispatched.acknowledgeDelivery()
|
|
deliveryAcknowledged = true
|
|
}
|
|
} catch (error) {
|
|
if (dispatched !== undefined && !deliveryAcknowledged) {
|
|
await Promise.resolve(
|
|
dispatched.failDelivery(error)
|
|
).catch(() => undefined)
|
|
}
|
|
if (!outgoing.headersSent && !outgoing.destroyed) {
|
|
const failure = httpFailure(error, cancellation.signal)
|
|
sendError(outgoing, failure.status, failure.code)
|
|
}
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
incoming.off('aborted', onAborted)
|
|
outgoing.off('close', onResponseClose)
|
|
if (dispatchHeld) {
|
|
this.#releaseDispatch(dispatchKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
async #acquireDispatch(key: string, signal: AbortSignal): Promise<void> {
|
|
signal.throwIfAborted()
|
|
const queue = this.#dispatches.get(key)
|
|
if (!queue) {
|
|
this.#dispatches.set(key, [])
|
|
return
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
const waiter: DispatchWaiter = { resolve, reject, signal }
|
|
waiter.abort = (): void => {
|
|
const index = queue.indexOf(waiter)
|
|
if (index >= 0) {
|
|
queue.splice(index, 1)
|
|
}
|
|
reject(signal.reason)
|
|
}
|
|
signal.addEventListener('abort', waiter.abort, { once: true })
|
|
queue.push(waiter)
|
|
})
|
|
}
|
|
|
|
#releaseDispatch(key: string): void {
|
|
const queue = this.#dispatches.get(key)
|
|
while (queue && queue.length > 0) {
|
|
const waiter = queue.shift()!
|
|
if (waiter.abort !== undefined) {
|
|
waiter.signal.removeEventListener('abort', waiter.abort)
|
|
}
|
|
if (waiter.signal.aborted) {
|
|
waiter.reject(waiter.signal.reason)
|
|
continue
|
|
}
|
|
waiter.resolve()
|
|
return
|
|
}
|
|
this.#dispatches.delete(key)
|
|
}
|
|
}
|
|
|
|
export async function runOpenCodeModelBridgeHelper(options: {
|
|
socketPath: string
|
|
protocol: ModelBridgeProtocol
|
|
model: string
|
|
supportsImageInput: boolean
|
|
sharedSessions?: boolean
|
|
opencodeEntrypoint: string
|
|
environment?: Readonly<NodeJS.ProcessEnv>
|
|
spawn?: ModelBridgeHelperSpawn
|
|
}): Promise<number> {
|
|
const socketPath = normalizedAbsolutePath(
|
|
options.socketPath,
|
|
'Model bridge socket'
|
|
)
|
|
const opencodeEntrypoint = normalizedAbsolutePath(
|
|
options.opencodeEntrypoint,
|
|
'OpenCode entrypoint'
|
|
)
|
|
const exchange = createUnixModelBridgeExchange({ socketPath })
|
|
const proxy = new ModelBridgeLoopbackProxy({ exchange, sharedSessions: options.sharedSessions })
|
|
let pluginDirectory: string | undefined
|
|
let child: ModelBridgeHelperChild | undefined
|
|
let stopping = false
|
|
const stop = (): void => {
|
|
if (stopping) return
|
|
stopping = true
|
|
child?.kill?.('SIGTERM')
|
|
}
|
|
process.on('SIGTERM', stop)
|
|
process.on('SIGINT', stop)
|
|
try {
|
|
const origin = await proxy.listen()
|
|
const launchRoot = join(dirname(socketPath), 'goodbuddy-runtime-launch')
|
|
await fs.mkdir(launchRoot, { recursive: true, mode: 0o700 })
|
|
pluginDirectory = await mkdtemp(join(launchRoot, 'goodbuddy-opencode-plugin-'))
|
|
const pluginPath = join(pluginDirectory, 'subagent.mjs')
|
|
await fs.writeFile(pluginPath, openCodeSubagentPluginSource(options.sharedSessions ? origin : undefined), 'utf8')
|
|
const config = createOpenCodeModelBridgeProviderConfig({
|
|
protocol: options.protocol,
|
|
model: options.model,
|
|
loopbackOrigin: origin,
|
|
supportsImageInput: options.supportsImageInput
|
|
})
|
|
const environment = credentialFreeHelperEnvironment(
|
|
options.environment ?? process.env,
|
|
JSON.stringify({
|
|
...config,
|
|
plugin: [pathToFileURL(pluginPath).href]
|
|
})
|
|
)
|
|
if (stopping) return 128
|
|
if (options.sharedSessions) {
|
|
process.stdout.write(JSON.stringify({
|
|
jsonrpc: '2.0', method: 'goodbuddy/modelBridgeReady', params: { origin }
|
|
}) + '\n')
|
|
}
|
|
child = (options.spawn ?? defaultHelperSpawn)(
|
|
opencodeEntrypoint,
|
|
['acp'],
|
|
{
|
|
shell: false,
|
|
stdio: 'inherit',
|
|
env: environment
|
|
}
|
|
)
|
|
const runningChild = child
|
|
return await new Promise<number>((resolveExit, reject) => {
|
|
runningChild.once('error', reject)
|
|
runningChild.once('close', (code, signal) => {
|
|
if (signal !== null) {
|
|
resolveExit(128)
|
|
return
|
|
}
|
|
resolveExit(code ?? 1)
|
|
})
|
|
})
|
|
} finally {
|
|
try {
|
|
await proxy.close()
|
|
} finally {
|
|
try {
|
|
if (pluginDirectory) await rm(pluginDirectory, { recursive: true, force: true })
|
|
} finally {
|
|
process.off('SIGTERM', stop)
|
|
process.off('SIGINT', stop)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function endResponseFlushed(
|
|
response: ServerResponse,
|
|
body: Uint8Array,
|
|
onFlushed: () => void
|
|
): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const onFinish = (): void => {
|
|
cleanup()
|
|
onFlushed()
|
|
resolve()
|
|
}
|
|
const onClose = (): void => {
|
|
if (response.writableFinished) {
|
|
onFinish()
|
|
return
|
|
}
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('transport-failed'))
|
|
}
|
|
const onError = (): void => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('transport-failed'))
|
|
}
|
|
const cleanup = (): void => {
|
|
response.off('finish', onFinish)
|
|
response.off('close', onClose)
|
|
response.off('error', onError)
|
|
}
|
|
response.once('finish', onFinish)
|
|
response.once('close', onClose)
|
|
response.once('error', onError)
|
|
response.end(body)
|
|
})
|
|
}
|
|
|
|
type DispatchWaiter = {
|
|
resolve: () => void
|
|
reject: (error: unknown) => void
|
|
signal: AbortSignal
|
|
abort?: () => void
|
|
}
|
|
|
|
export function createOpenCodeModelBridgeProviderConfig(input: {
|
|
protocol: ModelBridgeProtocol
|
|
model: string
|
|
loopbackOrigin: string
|
|
name?: string
|
|
supportsImageInput?: boolean
|
|
}): OpenCodeModelBridgeProviderConfig {
|
|
const model = boundedMetadataText(input.model, 'Model name')
|
|
const name = boundedMetadataText(
|
|
input.name ?? 'GoodBuddy Model Bridge',
|
|
'Provider name'
|
|
)
|
|
const origin = parseLoopbackOrigin(input.loopbackOrigin)
|
|
const descriptor = providerDescriptor(input.protocol)
|
|
const npm = descriptor.npm
|
|
const providerId = descriptor.id
|
|
const baseURL = `${origin}/v1`
|
|
return {
|
|
model: openCodeModelBridgeModelId(input.protocol, model),
|
|
snapshot: false,
|
|
permission: 'allow',
|
|
// GoodBuddy owns conversation titles. A first-prompt title request can
|
|
// race the real ACP prompt, abandon its Provider response, and correctly
|
|
// poison the no-replay ledger before the user request can run.
|
|
agent: {
|
|
title: {
|
|
disable: true
|
|
},
|
|
build: {
|
|
permission: 'allow'
|
|
}
|
|
},
|
|
provider: {
|
|
[providerId]: {
|
|
name,
|
|
npm,
|
|
options: {
|
|
baseURL,
|
|
...(descriptor.requiresSdkAuthSentinel
|
|
? { apiKey: MODEL_BRIDGE_SDK_AUTH_SENTINEL }
|
|
: {})
|
|
},
|
|
models: {
|
|
[model]: {
|
|
name,
|
|
attachment: input.supportsImageInput === true,
|
|
modalities: {
|
|
input:
|
|
input.supportsImageInput === true
|
|
? ['text', 'image']
|
|
: ['text'],
|
|
output: ['text']
|
|
},
|
|
provider: { npm }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function openCodeModelBridgeModelId(
|
|
protocol: ModelBridgeProtocol,
|
|
modelInput: string
|
|
): string {
|
|
const model = boundedMetadataText(modelInput, 'Model name')
|
|
return `${providerDescriptor(protocol).id}/${model}`
|
|
}
|
|
|
|
async function parseHttpRequest(
|
|
incoming: IncomingMessage,
|
|
routeToken: string
|
|
): Promise<RemoteModelGatewayRequest> {
|
|
if (incoming.method !== 'POST') {
|
|
throw new HttpRequestError(405, 'method-not-allowed')
|
|
}
|
|
const path = remoteModelApiPathSchema.safeParse(
|
|
authenticatedApiPath(incoming.url, routeToken)
|
|
)
|
|
if (!path.success) {
|
|
throw new HttpRequestError(404, 'path-not-allowed')
|
|
}
|
|
validateInboundHeaders(
|
|
incoming.headers,
|
|
incoming.rawHeaders,
|
|
path.data
|
|
)
|
|
const declaredLength = parseContentLength(
|
|
incoming.headers['content-length']
|
|
)
|
|
if (declaredLength === undefined) {
|
|
throw new HttpRequestError(411, 'content-length-required')
|
|
}
|
|
if (
|
|
declaredLength !== undefined &&
|
|
declaredLength >
|
|
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes
|
|
) {
|
|
incoming.resume()
|
|
throw new HttpRequestError(413, 'request-too-large')
|
|
}
|
|
const body = await readBoundedBody(incoming)
|
|
if (
|
|
declaredLength !== undefined &&
|
|
body.byteLength !== declaredLength
|
|
) {
|
|
throw new HttpRequestError(400, 'request-invalid')
|
|
}
|
|
const headers: Record<string, string> = {}
|
|
for (const name of FORWARDED_HEADER_NAMES) {
|
|
const value = incoming.headers[name]
|
|
if (typeof value === 'string') {
|
|
headers[name] = value
|
|
}
|
|
}
|
|
const parsed = remoteModelGatewayRequestSchema.safeParse({
|
|
method: 'POST',
|
|
path: path.data,
|
|
headers,
|
|
bodyBase64: body.toString('base64')
|
|
})
|
|
if (!parsed.success) {
|
|
throw new HttpRequestError(400, 'request-invalid')
|
|
}
|
|
return parsed.data
|
|
}
|
|
|
|
function validateInboundHeaders(
|
|
headers: IncomingHttpHeaders,
|
|
rawHeaders: readonly string[],
|
|
path: RemoteModelGatewayRequest['path']
|
|
): void {
|
|
const entries = Object.entries(headers)
|
|
if (entries.length > MAXIMUM_HTTP_HEADER_COUNT) {
|
|
throw new HttpRequestError(431, 'headers-rejected')
|
|
}
|
|
const securityCriticalNames = new Set([
|
|
'accept',
|
|
'connection',
|
|
'content-length',
|
|
'content-type',
|
|
'host',
|
|
'transfer-encoding',
|
|
...CREDENTIAL_INBOUND_HEADER_NAMES
|
|
])
|
|
const seen = new Set<string>()
|
|
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
const name = rawHeaders[index]?.toLowerCase()
|
|
if (
|
|
name !== undefined &&
|
|
securityCriticalNames.has(name) &&
|
|
seen.has(name)
|
|
) {
|
|
throw new HttpRequestError(400, 'headers-rejected')
|
|
}
|
|
if (name !== undefined) {
|
|
seen.add(name)
|
|
}
|
|
}
|
|
for (const [name, value] of entries) {
|
|
if (
|
|
!/^[a-z0-9!#$%&'*+.^_`|~-]+$/u.test(name) ||
|
|
Array.isArray(value) ||
|
|
(CREDENTIAL_INBOUND_HEADER_NAMES.has(name) &&
|
|
!isExpectedSdkAuthenticationHeader(path, name, value))
|
|
) {
|
|
throw new HttpRequestError(400, 'headers-rejected')
|
|
}
|
|
if (
|
|
value !== undefined &&
|
|
(value.length >
|
|
REMOTE_MODEL_GATEWAY_LIMITS.maximumHeaderValueBytes ||
|
|
/[\r\n]/u.test(value))
|
|
) {
|
|
throw new HttpRequestError(431, 'headers-rejected')
|
|
}
|
|
if (name === 'transfer-encoding' && value !== undefined) {
|
|
throw new HttpRequestError(400, 'headers-rejected')
|
|
}
|
|
}
|
|
}
|
|
|
|
function isExpectedSdkAuthenticationHeader(
|
|
path: RemoteModelGatewayRequest['path'],
|
|
name: string,
|
|
value: string | undefined
|
|
): boolean {
|
|
if (path === '/v1/messages') {
|
|
return (
|
|
name === 'x-api-key' &&
|
|
value === MODEL_BRIDGE_SDK_AUTH_SENTINEL
|
|
)
|
|
}
|
|
if (path === '/responses' || path === '/v1/responses') {
|
|
return (
|
|
name === 'authorization' &&
|
|
value === `Bearer ${MODEL_BRIDGE_SDK_AUTH_SENTINEL}`
|
|
)
|
|
}
|
|
if (
|
|
path === '/chat/completions' ||
|
|
path === '/v1/chat/completions'
|
|
) {
|
|
return (
|
|
name === 'authorization' &&
|
|
value === `Bearer ${MODEL_BRIDGE_SDK_AUTH_SENTINEL}`
|
|
)
|
|
}
|
|
return false
|
|
}
|
|
|
|
function parseContentLength(
|
|
value: string | undefined
|
|
): number | undefined {
|
|
if (value === undefined) {
|
|
return undefined
|
|
}
|
|
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) {
|
|
throw new HttpRequestError(400, 'request-invalid')
|
|
}
|
|
const length = Number(value)
|
|
if (!Number.isSafeInteger(length)) {
|
|
throw new HttpRequestError(413, 'request-too-large')
|
|
}
|
|
return length
|
|
}
|
|
|
|
async function readBoundedBody(
|
|
incoming: IncomingMessage
|
|
): Promise<Buffer> {
|
|
const chunks: Buffer[] = []
|
|
let total = 0
|
|
for await (const untrustedChunk of incoming) {
|
|
const chunk = Buffer.isBuffer(untrustedChunk)
|
|
? untrustedChunk
|
|
: Buffer.from(untrustedChunk as Uint8Array)
|
|
total += chunk.byteLength
|
|
if (
|
|
total >
|
|
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes
|
|
) {
|
|
throw new HttpRequestError(413, 'request-too-large')
|
|
}
|
|
chunks.push(chunk)
|
|
}
|
|
return Buffer.concat(chunks, total)
|
|
}
|
|
|
|
function sendError(
|
|
response: ServerResponse,
|
|
status: number,
|
|
code: string
|
|
): void {
|
|
const body = Buffer.from(JSON.stringify({ error: { code } }), 'utf8')
|
|
response.statusCode = status
|
|
response.setHeader('content-type', 'application/json')
|
|
response.setHeader('content-length', body.byteLength)
|
|
response.end(body)
|
|
}
|
|
|
|
function httpFailure(
|
|
error: unknown,
|
|
signal: AbortSignal
|
|
): { status: number; code: string } {
|
|
if (error instanceof HttpRequestError) {
|
|
return { status: error.status, code: error.code }
|
|
}
|
|
const reason = signal.reason
|
|
if (
|
|
(signal.aborted &&
|
|
reason instanceof ModelBridgeBrokerError &&
|
|
reason.code === 'request-timeout') ||
|
|
(error instanceof ModelBridgeBrokerError &&
|
|
error.code === 'request-timeout')
|
|
) {
|
|
return { status: 504, code: 'bridge-timeout' }
|
|
}
|
|
if (
|
|
signal.aborted ||
|
|
(error instanceof ModelBridgeBrokerError &&
|
|
error.code === 'request-cancelled')
|
|
) {
|
|
return { status: 499, code: 'request-cancelled' }
|
|
}
|
|
return { status: 502, code: 'bridge-failed' }
|
|
}
|
|
|
|
function providerDescriptor(protocol: ModelBridgeProtocol): {
|
|
id: string
|
|
npm:
|
|
| '@ai-sdk/anthropic'
|
|
| '@ai-sdk/openai-compatible'
|
|
| '@ai-sdk/openai'
|
|
requiresSdkAuthSentinel: boolean
|
|
} {
|
|
switch (protocol) {
|
|
case 'anthropic-messages':
|
|
return {
|
|
id: 'goodbuddy-anthropic',
|
|
npm: '@ai-sdk/anthropic',
|
|
requiresSdkAuthSentinel: true
|
|
}
|
|
case 'openai-chat-completions':
|
|
return {
|
|
id: 'goodbuddy-openai-chat',
|
|
npm: '@ai-sdk/openai-compatible',
|
|
requiresSdkAuthSentinel: true
|
|
}
|
|
case 'openai-responses':
|
|
return {
|
|
id: 'goodbuddy-openai-responses',
|
|
npm: '@ai-sdk/openai',
|
|
requiresSdkAuthSentinel: true
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeExchangeResult(
|
|
value: Awaited<ReturnType<ModelBridgeExchange>>
|
|
): ModelBridgeExchangeResult {
|
|
if (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
'response' in value &&
|
|
typeof value.acknowledgeDelivery === 'function' &&
|
|
typeof value.failDelivery === 'function'
|
|
) {
|
|
return value
|
|
}
|
|
return {
|
|
response: remoteModelGatewayResponseSchema.parse(value),
|
|
acknowledgeDelivery: async () => undefined,
|
|
failDelivery: () => undefined
|
|
}
|
|
}
|
|
|
|
function defaultHelperSpawn(
|
|
executable: string,
|
|
args: readonly string[],
|
|
options: {
|
|
shell: false
|
|
stdio: 'inherit'
|
|
env: Readonly<NodeJS.ProcessEnv>
|
|
}
|
|
): ModelBridgeHelperChild {
|
|
return nodeSpawn(executable, [...args], {
|
|
shell: options.shell,
|
|
stdio: options.stdio,
|
|
env: options.env
|
|
})
|
|
}
|
|
|
|
function credentialFreeHelperEnvironment(
|
|
source: Readonly<NodeJS.ProcessEnv>,
|
|
config: string
|
|
): Readonly<NodeJS.ProcessEnv> {
|
|
const allowed = [
|
|
'HOME',
|
|
'LANG',
|
|
'LC_ALL',
|
|
'PATH',
|
|
'TMPDIR',
|
|
'XDG_CACHE_HOME',
|
|
'XDG_CONFIG_HOME',
|
|
'XDG_DATA_HOME',
|
|
'XDG_STATE_HOME'
|
|
] as const
|
|
const environment: NodeJS.ProcessEnv = {
|
|
...runtimePrivacyEnvironment,
|
|
OPENCODE_CONFIG_CONTENT: config,
|
|
|
|
OPENCODE_ENABLE_QUESTION_TOOL: 'true'
|
|
}
|
|
for (const name of allowed) {
|
|
const value = source[name]
|
|
if (
|
|
value === undefined ||
|
|
value.includes('\0') ||
|
|
/(?:key|secret|token|authorization)/iu.test(name)
|
|
) {
|
|
throw new Error(`Fixed helper environment is missing ${name}`)
|
|
}
|
|
environment[name] = value
|
|
}
|
|
for (const name of [
|
|
'OPENCODE_CONFIG_DIR',
|
|
'OPENCODE_DISABLE_AUTOUPDATE',
|
|
'OPENCODE_DISABLE_CLAUDE_CODE_SKILLS',
|
|
'OPENCODE_DISABLE_EMBEDDED_WEB_UI',
|
|
'OPENCODE_DISABLE_EXTERNAL_SKILLS',
|
|
'OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER',
|
|
'OPENCODE_DISABLE_LSP_DOWNLOAD',
|
|
'OPENCODE_DISABLE_MODELS_FETCH',
|
|
'OPENCODE_DISABLE_PROJECT_CONFIG',
|
|
'OPENCODE_DISABLE_SHARE'
|
|
]) {
|
|
const value = source[name]
|
|
if (value !== undefined) {
|
|
if (value.includes('\0')) {
|
|
throw new Error(`Invalid helper environment value for ${name}`)
|
|
}
|
|
environment[name] = value
|
|
}
|
|
}
|
|
return environment
|
|
}
|
|
|
|
function normalizedAbsolutePath(value: string, label: string): string {
|
|
if (
|
|
value.includes('\0') ||
|
|
!isAbsolute(value) ||
|
|
resolve(value) !== value
|
|
) {
|
|
throw new Error(`${label} must be a normalized absolute path`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function parseLoopbackOrigin(input: string): string {
|
|
let url: URL
|
|
try {
|
|
url = new URL(input)
|
|
} catch {
|
|
throw new Error('Model bridge origin is invalid')
|
|
}
|
|
if (
|
|
url.protocol !== 'http:' ||
|
|
url.hostname !== LOOPBACK_HOST ||
|
|
url.username ||
|
|
url.password ||
|
|
!MODEL_BRIDGE_ROUTE_TOKEN_PATTERN.test(
|
|
url.pathname.slice(1)
|
|
) ||
|
|
url.search ||
|
|
url.hash ||
|
|
url.port === ''
|
|
) {
|
|
throw new Error(
|
|
'Model bridge origin must be authenticated loopback HTTP'
|
|
)
|
|
}
|
|
return `${url.origin}${url.pathname}`
|
|
}
|
|
|
|
function parseModelBridgeRouteToken(value: string): string {
|
|
if (!MODEL_BRIDGE_ROUTE_TOKEN_PATTERN.test(value)) {
|
|
throw new Error('Model bridge route capability is invalid')
|
|
}
|
|
return value
|
|
}
|
|
|
|
function authenticatedApiPath(
|
|
input: string | undefined,
|
|
routeToken: string
|
|
): string | undefined {
|
|
const prefix = `/${routeToken}`
|
|
return input?.startsWith(`${prefix}/`)
|
|
? input.slice(prefix.length)
|
|
: undefined
|
|
}
|
|
|
|
function boundedMetadataText(
|
|
value: string,
|
|
label: string
|
|
): string {
|
|
if (
|
|
value.length < 1 ||
|
|
Buffer.byteLength(value, 'utf8') > 512 ||
|
|
/[\0\r\n]/u.test(value)
|
|
) {
|
|
throw new Error(`${label} is invalid`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
function listenOnLoopback(server: Server): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const onError = (error: Error): void => {
|
|
cleanup()
|
|
reject(error)
|
|
}
|
|
const onListening = (): void => {
|
|
cleanup()
|
|
resolve()
|
|
}
|
|
const cleanup = (): void => {
|
|
server.off('error', onError)
|
|
server.off('listening', onListening)
|
|
}
|
|
server.once('error', onError)
|
|
server.once('listening', onListening)
|
|
server.listen({
|
|
host: LOOPBACK_HOST,
|
|
port: 0,
|
|
exclusive: true
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
class HttpRequestError extends Error {
|
|
readonly status: number
|
|
readonly code: string
|
|
|
|
constructor(status: number, code: string) {
|
|
super('Model bridge HTTP request was rejected')
|
|
this.name = 'HttpRequestError'
|
|
this.status = status
|
|
this.code = code
|
|
}
|
|
}
|