Managed SSH previously tied remote Agent and Runtime artifacts to desktop packaging and Host activation. Settings now manages verified Linux x64 and arm64 .gbagent packages explicitly, with source-aligned download and update plus offline import and export. Each package contains the signed Agent, pinned Node, and compatible OpenCode Runtime, and project activation never downloads one automatically. Dedicated native CI and a protected release workflow publish deterministic, signed cumulative catalogs to GitHub and Beijing OSS without replacing the desktop Latest release. Desktop packaging is metadata-only, remains independent of Agent artifacts and signing credentials, and Linux desktop releases add RPM installers. The managed SSH path also keeps model credentials in Main, enforces Ask as read-only, verifies signed Agent and Runtime inputs, preserves ACP recovery, and reports Host update cancellation and failures accurately. Release note: 远程项目现可在“设置”中手动下载、更新或离线导入签名 Agent 包,桌面安装包与远程组件发布相互独立;Linux 桌面版同时新增 RPM 安装包。
827 lines
21 KiB
TypeScript
827 lines
21 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import {
|
|
chmodSync,
|
|
lstatSync,
|
|
type Stats,
|
|
unlinkSync
|
|
} from 'node:fs'
|
|
import {
|
|
createConnection,
|
|
createServer,
|
|
type Server,
|
|
type Socket
|
|
} from 'node:net'
|
|
import { dirname, join } from 'node:path'
|
|
import { TextDecoder } from 'node:util'
|
|
import { z } from 'zod'
|
|
import {
|
|
REMOTE_MODEL_GATEWAY_LIMITS,
|
|
remoteModelGatewayRequestSchema,
|
|
remoteModelGatewayResponseSchema,
|
|
type RemoteModelGatewayRequest,
|
|
type RemoteModelGatewayResponse
|
|
} from '../shared/remote-model-gateway-contracts'
|
|
import {
|
|
assertAbsoluteManagedPath,
|
|
ensurePrivateDirectory
|
|
} from './managed-paths'
|
|
import { AgentUnsupportedError } from './errors'
|
|
import {
|
|
boundedInteger,
|
|
closeServer,
|
|
isNodeError,
|
|
raceWithAbort,
|
|
settleWithin
|
|
} from './async-utils'
|
|
|
|
export const MODEL_BRIDGE_BROKER_SOCKET_NAME =
|
|
'model-bridge.sock'
|
|
|
|
const BROKER_PROTOCOL_VERSION = 1
|
|
const DEFAULT_MAXIMUM_CONNECTIONS = 8
|
|
const DEFAULT_REQUEST_TIMEOUT_MS = 150_000
|
|
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000
|
|
const CLOSE_TIMEOUT_MS = 2_000
|
|
const MAXIMUM_UNIX_SOCKET_PATH_BYTES = 107
|
|
const MAXIMUM_FRAME_BYTES =
|
|
Math.ceil(
|
|
REMOTE_MODEL_GATEWAY_LIMITS.maximumResponseBodyBytes / 3
|
|
) *
|
|
4 +
|
|
64 * 1_024
|
|
|
|
const requestIdSchema = z
|
|
.string()
|
|
.min(1)
|
|
.max(128)
|
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u)
|
|
|
|
const brokerRequestPacketSchema = z
|
|
.object({
|
|
version: z.literal(BROKER_PROTOCOL_VERSION),
|
|
type: z.literal('model-request'),
|
|
requestId: requestIdSchema,
|
|
request: remoteModelGatewayRequestSchema
|
|
})
|
|
.strict()
|
|
|
|
const brokerSuccessPacketSchema = z
|
|
.object({
|
|
version: z.literal(BROKER_PROTOCOL_VERSION),
|
|
type: z.literal('model-response'),
|
|
requestId: requestIdSchema,
|
|
ok: z.literal(true),
|
|
response: remoteModelGatewayResponseSchema
|
|
})
|
|
.strict()
|
|
|
|
const brokerFailureCodeSchema = z.enum([
|
|
'request-invalid',
|
|
'request-timeout',
|
|
'request-cancelled',
|
|
'dispatch-failed'
|
|
])
|
|
|
|
const brokerFailurePacketSchema = z
|
|
.object({
|
|
version: z.literal(BROKER_PROTOCOL_VERSION),
|
|
type: z.literal('model-response'),
|
|
requestId: requestIdSchema,
|
|
ok: z.literal(false),
|
|
error: z
|
|
.object({
|
|
code: brokerFailureCodeSchema
|
|
})
|
|
.strict()
|
|
})
|
|
.strict()
|
|
|
|
const brokerResponsePacketSchema = z.discriminatedUnion('ok', [
|
|
brokerSuccessPacketSchema,
|
|
brokerFailurePacketSchema
|
|
])
|
|
|
|
const brokerDeliveryPacketSchema = z
|
|
.object({
|
|
version: z.literal(BROKER_PROTOCOL_VERSION),
|
|
type: z.literal('response-delivered'),
|
|
requestId: requestIdSchema
|
|
})
|
|
.strict()
|
|
|
|
const brokerDeliveryAcceptedPacketSchema = z
|
|
.object({
|
|
version: z.literal(BROKER_PROTOCOL_VERSION),
|
|
type: z.literal('delivery-accepted'),
|
|
requestId: requestIdSchema
|
|
})
|
|
.strict()
|
|
|
|
export type ModelBridgeDispatchContext = {
|
|
requestId: string
|
|
signal: AbortSignal
|
|
}
|
|
|
|
export type ModelBridgeExchangeResult = {
|
|
response: RemoteModelGatewayResponse
|
|
acknowledgeDelivery(): Promise<void>
|
|
failDelivery(reason?: unknown): void | Promise<void>
|
|
}
|
|
|
|
export type ModelBridgeBrokerDispatch = (
|
|
request: RemoteModelGatewayRequest,
|
|
context: ModelBridgeDispatchContext
|
|
) => Promise<ModelBridgeExchangeResult | RemoteModelGatewayResponse>
|
|
|
|
export type ModelBridgeExchange = (
|
|
request: RemoteModelGatewayRequest,
|
|
context: ModelBridgeDispatchContext
|
|
) => Promise<ModelBridgeExchangeResult | RemoteModelGatewayResponse>
|
|
|
|
export class ModelBridgeBrokerError extends Error {
|
|
readonly code:
|
|
| 'request-invalid'
|
|
| 'request-timeout'
|
|
| 'request-cancelled'
|
|
| 'dispatch-failed'
|
|
| 'transport-failed'
|
|
|
|
constructor(code: ModelBridgeBrokerError['code']) {
|
|
super(modelBridgeBrokerErrorMessage(code))
|
|
this.name = 'ModelBridgeBrokerError'
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
export class ModelBridgeBrokerServer {
|
|
readonly socketPath: string
|
|
readonly #dispatch: ModelBridgeBrokerDispatch
|
|
readonly #maximumConnections: number
|
|
readonly #requestTimeoutMs: number
|
|
readonly #connections = new Set<Socket>()
|
|
#server?: Server
|
|
#socketIdentity?: FileIdentity
|
|
#closing = false
|
|
#closePromise?: Promise<void>
|
|
|
|
constructor(options: {
|
|
scratchDirectory: string
|
|
dispatch: ModelBridgeBrokerDispatch
|
|
maximumConnections?: number
|
|
requestTimeoutMs?: number
|
|
}) {
|
|
const scratchDirectory = assertAbsoluteManagedPath(
|
|
options.scratchDirectory
|
|
)
|
|
this.socketPath = assertAbsoluteManagedPath(
|
|
join(scratchDirectory, MODEL_BRIDGE_BROKER_SOCKET_NAME)
|
|
)
|
|
if (
|
|
process.platform !== 'win32' &&
|
|
Buffer.byteLength(this.socketPath, 'utf8') >
|
|
MAXIMUM_UNIX_SOCKET_PATH_BYTES
|
|
) {
|
|
throw new Error('Model bridge endpoint path is too long')
|
|
}
|
|
this.#dispatch = options.dispatch
|
|
this.#maximumConnections = boundedInteger(
|
|
options.maximumConnections ?? DEFAULT_MAXIMUM_CONNECTIONS,
|
|
1,
|
|
64,
|
|
'Model bridge connection limit'
|
|
)
|
|
this.#requestTimeoutMs = boundedInteger(
|
|
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1,
|
|
300_000,
|
|
'Model bridge request timeout'
|
|
)
|
|
}
|
|
|
|
async listen(): Promise<void> {
|
|
if (process.platform === 'win32') {
|
|
throw new AgentUnsupportedError(
|
|
'Unix model bridge brokers are unavailable on Windows',
|
|
'platform-incompatible'
|
|
)
|
|
}
|
|
if (this.#server !== undefined || this.#closing) {
|
|
throw new Error('Model bridge broker is already active')
|
|
}
|
|
ensurePrivateDirectory(dirname(this.socketPath), {
|
|
create: false
|
|
})
|
|
removeOwnedStaleSocket(this.socketPath)
|
|
|
|
const server = createServer(
|
|
{ pauseOnConnect: true },
|
|
(socket) => {
|
|
if (
|
|
this.#closing ||
|
|
this.#connections.size >= this.#maximumConnections
|
|
) {
|
|
socket.destroy()
|
|
return
|
|
}
|
|
this.#connections.add(socket)
|
|
socket.once('close', () => {
|
|
this.#connections.delete(socket)
|
|
})
|
|
void this.#serveOne(socket)
|
|
}
|
|
)
|
|
this.#server = server
|
|
try {
|
|
await listenOnUnixSocket(server, this.socketPath)
|
|
const initialStat = lstatSync(this.socketPath)
|
|
this.#socketIdentity = fileIdentity(initialStat)
|
|
assertOwnedSocket(initialStat, 'Model bridge endpoint')
|
|
chmodSync(this.socketPath, 0o600)
|
|
const securedStat = lstatSync(this.socketPath)
|
|
assertSameFile(securedStat, this.#socketIdentity)
|
|
assertOwnedSocket(securedStat, 'Model bridge endpoint')
|
|
if ((securedStat.mode & 0o777) !== 0o600) {
|
|
throw new Error('Model bridge endpoint mode verification failed')
|
|
}
|
|
} 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.#closing = true
|
|
try {
|
|
for (const connection of this.#connections) {
|
|
connection.destroy()
|
|
}
|
|
await settleWithin(
|
|
server === undefined
|
|
? Promise.resolve()
|
|
: closeServer(server),
|
|
CLOSE_TIMEOUT_MS
|
|
)
|
|
} finally {
|
|
this.#connections.clear()
|
|
removeSocketIfIdentityMatches(
|
|
this.socketPath,
|
|
this.#socketIdentity
|
|
)
|
|
this.#socketIdentity = undefined
|
|
this.#closing = false
|
|
}
|
|
}
|
|
|
|
async #serveOne(socket: Socket): Promise<void> {
|
|
const cancellation = new AbortController()
|
|
const onClose = (): void => {
|
|
cancellation.abort(
|
|
new ModelBridgeBrokerError('request-cancelled')
|
|
)
|
|
}
|
|
socket.once('close', onClose)
|
|
const timeout = setTimeout(() => {
|
|
cancellation.abort(
|
|
new ModelBridgeBrokerError('request-timeout')
|
|
)
|
|
}, this.#requestTimeoutMs)
|
|
timeout.unref?.()
|
|
|
|
let requestId = `invalid-${randomUUID()}`
|
|
let dispatched: ModelBridgeExchangeResult | undefined
|
|
let delivered = false
|
|
try {
|
|
const packet = brokerRequestPacketSchema.parse(
|
|
await readPacket(socket, cancellation.signal)
|
|
)
|
|
requestId = packet.requestId
|
|
dispatched = normalizeExchangeResult(await raceWithAbort(
|
|
this.#dispatch(packet.request, {
|
|
requestId,
|
|
signal: cancellation.signal
|
|
}),
|
|
cancellation.signal
|
|
))
|
|
const response = remoteModelGatewayResponseSchema.parse(
|
|
dispatched.response
|
|
)
|
|
cancellation.signal.throwIfAborted()
|
|
await writePacket(socket, {
|
|
version: BROKER_PROTOCOL_VERSION,
|
|
type: 'model-response',
|
|
requestId,
|
|
ok: true,
|
|
response
|
|
})
|
|
const delivery = brokerDeliveryPacketSchema.parse(
|
|
await readPacket(socket, cancellation.signal)
|
|
)
|
|
if (delivery.requestId !== requestId) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
await raceWithAbort(
|
|
Promise.resolve(dispatched.acknowledgeDelivery()),
|
|
cancellation.signal
|
|
)
|
|
delivered = true
|
|
await writePacket(socket, {
|
|
version: BROKER_PROTOCOL_VERSION,
|
|
type: 'delivery-accepted',
|
|
requestId
|
|
})
|
|
} catch (error) {
|
|
if (dispatched !== undefined && !delivered) {
|
|
await Promise.resolve(
|
|
dispatched.failDelivery(error)
|
|
).catch(() => undefined)
|
|
} else if (!socket.destroyed) {
|
|
await writeFailure(
|
|
socket,
|
|
requestId,
|
|
failureCode(error, cancellation.signal)
|
|
).catch(() => undefined)
|
|
}
|
|
} finally {
|
|
clearTimeout(timeout)
|
|
socket.off('close', onClose)
|
|
if (!socket.destroyed) {
|
|
socket.end()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createUnixModelBridgeExchange(options: {
|
|
socketPath: string
|
|
connectTimeoutMs?: number
|
|
requestTimeoutMs?: number
|
|
}): ModelBridgeExchange {
|
|
const socketPath = assertAbsoluteManagedPath(options.socketPath)
|
|
const connectTimeoutMs = boundedInteger(
|
|
options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
1,
|
|
60_000,
|
|
'Model bridge connect timeout'
|
|
)
|
|
const requestTimeoutMs = boundedInteger(
|
|
options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1,
|
|
300_000,
|
|
'Model bridge request timeout'
|
|
)
|
|
|
|
return async (request, context) => {
|
|
const parsedRequest = remoteModelGatewayRequestSchema.parse(request)
|
|
const requestId = requestIdSchema.parse(context.requestId)
|
|
context.signal.throwIfAborted()
|
|
const socket = createConnection(socketPath)
|
|
const timeoutController = new AbortController()
|
|
const timeout = setTimeout(() => {
|
|
timeoutController.abort(
|
|
new ModelBridgeBrokerError('request-timeout')
|
|
)
|
|
socket.destroy()
|
|
}, requestTimeoutMs)
|
|
timeout.unref?.()
|
|
const onAbort = (): void => {
|
|
socket.destroy()
|
|
}
|
|
context.signal.addEventListener('abort', onAbort, { once: true })
|
|
let responseHandedOff = false
|
|
const cleanup = (): void => {
|
|
clearTimeout(timeout)
|
|
context.signal.removeEventListener('abort', onAbort)
|
|
socket.destroy()
|
|
}
|
|
try {
|
|
await waitForConnection(
|
|
socket,
|
|
Math.min(connectTimeoutMs, requestTimeoutMs),
|
|
context.signal
|
|
)
|
|
await writePacket(socket, {
|
|
version: BROKER_PROTOCOL_VERSION,
|
|
type: 'model-request',
|
|
requestId,
|
|
request: parsedRequest
|
|
})
|
|
const packet = brokerResponsePacketSchema.parse(
|
|
await readPacket(
|
|
socket,
|
|
AbortSignal.any([
|
|
context.signal,
|
|
timeoutController.signal
|
|
])
|
|
)
|
|
)
|
|
if (packet.requestId !== requestId) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
if (!packet.ok) {
|
|
throw new ModelBridgeBrokerError(packet.error.code)
|
|
}
|
|
responseHandedOff = true
|
|
let deliveryFinalized = false
|
|
return {
|
|
response: packet.response,
|
|
acknowledgeDelivery: async () => {
|
|
if (deliveryFinalized) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
deliveryFinalized = true
|
|
await writePacket(socket, {
|
|
version: BROKER_PROTOCOL_VERSION,
|
|
type: 'response-delivered',
|
|
requestId
|
|
})
|
|
const accepted = brokerDeliveryAcceptedPacketSchema.parse(
|
|
await readPacket(
|
|
socket,
|
|
AbortSignal.any([
|
|
context.signal,
|
|
timeoutController.signal
|
|
])
|
|
)
|
|
)
|
|
if (accepted.requestId !== requestId) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
cleanup()
|
|
},
|
|
failDelivery: () => {
|
|
deliveryFinalized = true
|
|
cleanup()
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (context.signal.aborted) {
|
|
throw new ModelBridgeBrokerError('request-cancelled')
|
|
}
|
|
if (timeoutController.signal.aborted) {
|
|
throw new ModelBridgeBrokerError('request-timeout')
|
|
}
|
|
if (error instanceof ModelBridgeBrokerError) {
|
|
throw error
|
|
}
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
} finally {
|
|
if (!responseHandedOff) {
|
|
cleanup()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type BrokerFailureCode = z.infer<
|
|
typeof brokerFailureCodeSchema
|
|
>
|
|
|
|
type FileIdentity = {
|
|
dev: bigint
|
|
ino: bigint
|
|
}
|
|
|
|
function failureCode(
|
|
error: unknown,
|
|
signal: AbortSignal
|
|
): BrokerFailureCode {
|
|
const reason = signal.reason
|
|
if (
|
|
signal.aborted &&
|
|
reason instanceof ModelBridgeBrokerError &&
|
|
reason.code === 'request-timeout'
|
|
) {
|
|
return 'request-timeout'
|
|
}
|
|
if (signal.aborted) {
|
|
return 'request-cancelled'
|
|
}
|
|
if (error instanceof z.ZodError || error instanceof SyntaxError) {
|
|
return 'request-invalid'
|
|
}
|
|
return 'dispatch-failed'
|
|
}
|
|
|
|
async function writeFailure(
|
|
socket: Socket,
|
|
requestId: string,
|
|
code: BrokerFailureCode
|
|
): Promise<void> {
|
|
await writePacket(socket, {
|
|
version: BROKER_PROTOCOL_VERSION,
|
|
type: 'model-response',
|
|
requestId,
|
|
ok: false,
|
|
error: { code }
|
|
})
|
|
}
|
|
|
|
async function readPacket(
|
|
socket: Socket,
|
|
signal: AbortSignal
|
|
): Promise<unknown> {
|
|
const lengthBytes = await readExactly(socket, 4, signal)
|
|
const length = lengthBytes.readUInt32BE(0)
|
|
if (length < 2 || length > MAXIMUM_FRAME_BYTES) {
|
|
throw new ModelBridgeBrokerError('request-invalid')
|
|
}
|
|
const contents = await readExactly(socket, length, signal)
|
|
const text = new TextDecoder('utf-8', { fatal: true }).decode(
|
|
contents
|
|
)
|
|
return JSON.parse(text) as unknown
|
|
}
|
|
|
|
async function writePacket(
|
|
socket: Socket,
|
|
value: unknown
|
|
): Promise<void> {
|
|
const contents = Buffer.from(JSON.stringify(value), 'utf8')
|
|
if (
|
|
contents.byteLength < 2 ||
|
|
contents.byteLength > MAXIMUM_FRAME_BYTES
|
|
) {
|
|
throw new ModelBridgeBrokerError('request-invalid')
|
|
}
|
|
const packet = Buffer.allocUnsafe(contents.byteLength + 4)
|
|
packet.writeUInt32BE(contents.byteLength, 0)
|
|
contents.copy(packet, 4)
|
|
await new Promise<void>((resolve, reject) => {
|
|
socket.write(packet, (error) => {
|
|
if (error === undefined || error === null) {
|
|
resolve()
|
|
} else {
|
|
reject(error)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
async function readExactly(
|
|
socket: Socket,
|
|
byteLength: number,
|
|
signal: AbortSignal
|
|
): Promise<Buffer> {
|
|
const chunks: Buffer[] = []
|
|
let remaining = byteLength
|
|
while (remaining > 0) {
|
|
signal.throwIfAborted()
|
|
const readableBytes = Math.min(
|
|
remaining,
|
|
socket.readableLength
|
|
)
|
|
if (readableBytes > 0) {
|
|
const chunk = socket.read(readableBytes) as Buffer | null
|
|
if (chunk === null || chunk.byteLength !== readableBytes) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
chunks.push(chunk)
|
|
remaining -= chunk.byteLength
|
|
continue
|
|
}
|
|
if (socket.readableEnded || socket.destroyed) {
|
|
throw new ModelBridgeBrokerError('transport-failed')
|
|
}
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onReadable = (): void => {
|
|
cleanup()
|
|
resolve()
|
|
}
|
|
const onClose = (): void => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('transport-failed'))
|
|
}
|
|
const onError = (): void => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('transport-failed'))
|
|
}
|
|
const onAbort = (): void => {
|
|
cleanup()
|
|
reject(signal.reason)
|
|
}
|
|
const cleanup = (): void => {
|
|
socket.off('readable', onReadable)
|
|
socket.off('close', onClose)
|
|
socket.off('end', onClose)
|
|
socket.off('error', onError)
|
|
signal.removeEventListener('abort', onAbort)
|
|
}
|
|
socket.once('readable', onReadable)
|
|
socket.once('close', onClose)
|
|
socket.once('end', onClose)
|
|
socket.once('error', onError)
|
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
if (signal.aborted) {
|
|
onAbort()
|
|
return
|
|
}
|
|
if (socket.readableLength > 0) {
|
|
onReadable()
|
|
return
|
|
}
|
|
if (socket.readableEnded || socket.destroyed) {
|
|
onClose()
|
|
}
|
|
})
|
|
}
|
|
return Buffer.concat(chunks, byteLength)
|
|
}
|
|
|
|
async function waitForConnection(
|
|
socket: Socket,
|
|
timeoutMs: number,
|
|
signal: AbortSignal
|
|
): Promise<void> {
|
|
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
await new Promise<void>((resolve, reject) => {
|
|
const onConnect = (): void => {
|
|
cleanup()
|
|
resolve()
|
|
}
|
|
const onError = (): void => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('transport-failed'))
|
|
}
|
|
const onAbort = (): void => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('request-cancelled'))
|
|
}
|
|
const cleanup = (): void => {
|
|
if (timeout !== undefined) {
|
|
clearTimeout(timeout)
|
|
}
|
|
socket.off('connect', onConnect)
|
|
socket.off('error', onError)
|
|
signal.removeEventListener('abort', onAbort)
|
|
}
|
|
timeout = setTimeout(() => {
|
|
cleanup()
|
|
reject(new ModelBridgeBrokerError('request-timeout'))
|
|
}, timeoutMs)
|
|
timeout.unref?.()
|
|
socket.once('connect', onConnect)
|
|
socket.once('error', onError)
|
|
signal.addEventListener('abort', onAbort, { once: true })
|
|
})
|
|
}
|
|
|
|
function listenOnUnixSocket(
|
|
server: Server,
|
|
socketPath: string
|
|
): 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(socketPath)
|
|
})
|
|
}
|
|
|
|
function removeOwnedStaleSocket(path: string): void {
|
|
let stat: Stats
|
|
try {
|
|
stat = lstatSync(path)
|
|
} catch (error) {
|
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
assertOwnedSocket(stat, 'Existing model bridge endpoint')
|
|
if ((stat.mode & 0o777) !== 0o600) {
|
|
throw new Error(
|
|
'Refusing to replace an insecure model bridge endpoint'
|
|
)
|
|
}
|
|
unlinkSync(path)
|
|
}
|
|
|
|
function removeSocketIfIdentityMatches(
|
|
path: string,
|
|
identity: FileIdentity | undefined
|
|
): void {
|
|
if (identity === undefined) {
|
|
return
|
|
}
|
|
let stat: Stats
|
|
try {
|
|
stat = lstatSync(path)
|
|
} catch (error) {
|
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
if (
|
|
stat.isSocket() &&
|
|
isOwnedByCurrentUser(stat) &&
|
|
sameFile(stat, identity)
|
|
) {
|
|
unlinkSync(path)
|
|
}
|
|
}
|
|
|
|
function assertOwnedSocket(stat: Stats, label: string): void {
|
|
if (
|
|
stat.isSymbolicLink() ||
|
|
!stat.isSocket() ||
|
|
!isOwnedByCurrentUser(stat)
|
|
) {
|
|
throw new Error(`${label} is not an owned Unix socket`)
|
|
}
|
|
}
|
|
|
|
function assertSameFile(
|
|
stat: Stats,
|
|
identity: FileIdentity
|
|
): void {
|
|
if (!sameFile(stat, identity)) {
|
|
throw new Error('Model bridge endpoint changed during setup')
|
|
}
|
|
}
|
|
|
|
function sameFile(stat: Stats, identity: FileIdentity): boolean {
|
|
return (
|
|
BigInt(stat.dev) === identity.dev &&
|
|
BigInt(stat.ino) === identity.ino
|
|
)
|
|
}
|
|
|
|
function fileIdentity(stat: Stats): FileIdentity {
|
|
return {
|
|
dev: BigInt(stat.dev),
|
|
ino: BigInt(stat.ino)
|
|
}
|
|
}
|
|
|
|
function isOwnedByCurrentUser(stat: Stats): boolean {
|
|
const uid = process.getuid?.()
|
|
return uid === undefined || stat.uid === uid
|
|
}
|
|
|
|
|
|
function modelBridgeBrokerErrorMessage(
|
|
code: ModelBridgeBrokerError['code']
|
|
): string {
|
|
switch (code) {
|
|
case 'request-invalid':
|
|
return 'Model bridge request was rejected'
|
|
case 'request-timeout':
|
|
return 'Model bridge request timed out'
|
|
case 'request-cancelled':
|
|
return 'Model bridge request was cancelled'
|
|
case 'dispatch-failed':
|
|
return 'Model bridge dispatch failed'
|
|
case 'transport-failed':
|
|
return 'Model bridge transport failed'
|
|
}
|
|
}
|
|
|
|
function normalizeExchangeResult(
|
|
value: ModelBridgeExchangeResult | RemoteModelGatewayResponse
|
|
): 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
|
|
}
|
|
}
|