Execute could stall on workspace-external access or native child-session permissions, while structured questions could be lost or answered incorrectly. Route owned OpenCode descendant interactions, correct Continue question and custom-answer handling, and resolve DSH Execute ACP permissions without a second approval. Ask remains read-only and unrelated sessions stay isolated. Restore the required DSH session projection service. Reuse offline OpenCode configuration by plugin version, remove unused npm shims from remote payloads, normalize dependency modes, and accept real compound descriptors up to 4 MiB. Fix intermittent Agent attach rejection and closed-handshake waits. Add focused and real-Runtime regressions, document interaction boundaries and validation evidence, and include the existing application/tool navigation design drafts. Those navigation changes are design-only, not implemented UI. Validation: full suite 3597 passed, 0 failed, 61 skipped across 338 files; typecheck, lint, release-note verification, whitespace and relative-link checks passed. Current-source Linux x64 installation, Ask/Execute, native subagent external writes, reconnect and stop/bootstrap passed with 10 completed real model calls, 13 native endpoint tests and 20 consecutive handshakes. System sleep/wake and release CI packaging remain unverified. Release note: 修复 Execute 访问工作区外目录或运行原生子代理时卡在权限等待的问题,并恢复 OpenCode 与 Continue 结构化问答的正确转交。修复远程 Agent 安装与重连失败;Ask 仍保持只读,任意终端密码或业务确认不会自动代答。
615 lines
19 KiB
TypeScript
615 lines
19 KiB
TypeScript
import {
|
|
chmodSync,
|
|
mkdtempSync,
|
|
rmSync
|
|
} from 'node:fs'
|
|
import {
|
|
createConnection,
|
|
createServer,
|
|
type Socket
|
|
} from 'node:net'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { PassThrough } from 'node:stream'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
AGENT_PROTOCOL_VERSION,
|
|
attachWelcomeSchema,
|
|
daemonCapabilitiesSchema,
|
|
daemonStatusSchema
|
|
} from '../shared/agent-protocol'
|
|
import {
|
|
decodeAgentFrame,
|
|
encodeAgentFrame,
|
|
type AgentFrame
|
|
} from '../shared/agent-protocol/frame'
|
|
import { ControllerRegistry } from './controller-registry'
|
|
import { EventJournal } from './event-journal'
|
|
import { InstallationChallengeVerifier } from './installation-challenge'
|
|
import {
|
|
attachRelay,
|
|
PrivateEndpoint,
|
|
probeAuthenticatedEndpoint,
|
|
type PeerIdentity
|
|
} from './private-endpoint'
|
|
import { AgentProtocolServer } from './protocol-server'
|
|
|
|
const runOnUnix = process.platform !== 'win32' ? it : it.skip
|
|
const temporaryPaths: string[] = []
|
|
const binaryDigest = `sha256:${'a'.repeat(64)}`
|
|
const endpointIdentity = {
|
|
installationId: 'installation-a',
|
|
binaryDigest,
|
|
daemonBootId: 'boot-a',
|
|
protocol: AGENT_PROTOCOL_VERSION
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
for (const path of temporaryPaths.splice(0)) {
|
|
rmSync(path, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('private endpoint lifecycle', () => {
|
|
runOnUnix.each(['-', '_'])('accepts a valid HMAC beginning with %s', async (prefix) => {
|
|
const root = privateTemporaryDirectory()
|
|
const verifier = new InstallationChallengeVerifier(Buffer.alloc(32, 7))
|
|
const attached = vi.fn()
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: join(root, 'agent.sock'),
|
|
peerIdentity: currentPeerIdentity(), challenge: verifier,
|
|
controllers: new ControllerRegistry(), ...endpointIdentity, onAttach: attached
|
|
})
|
|
await endpoint.listen()
|
|
const client = createConnection(join(root, 'agent.sock'))
|
|
try {
|
|
await onceConnected(client)
|
|
const challenge = await readPacket(client)
|
|
let clientNonce = ''
|
|
let response = ''
|
|
for (let index = 0; index < 4096 && !response.startsWith(prefix); index++) {
|
|
clientNonce = `client-${index}`
|
|
response = verifier.createResponse({
|
|
serverNonce: String(challenge.serverNonce), clientNonce, controllerId: 'controller-a'
|
|
})
|
|
}
|
|
expect(response.startsWith(prefix)).toBe(true)
|
|
await writePacket(client, {
|
|
type: 'response', version: 1, controllerId: 'controller-a', clientNonce,
|
|
protocol: AGENT_PROTOCOL_VERSION, response
|
|
})
|
|
expect(attachWelcomeSchema.parse(await readPacket(client)).installationId).toBe('installation-a')
|
|
await vi.waitFor(() => expect(attached).toHaveBeenCalledOnce())
|
|
} finally {
|
|
client.destroy()
|
|
await endpoint.close()
|
|
}
|
|
})
|
|
|
|
runOnUnix('rejects a failed health handshake instead of leaving an unsettled read', async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const socketPath = join(root, 'agent.sock')
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath, peerIdentity: currentPeerIdentity(),
|
|
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
|
|
controllers: new ControllerRegistry(), ...endpointIdentity, onAttach: vi.fn()
|
|
})
|
|
await endpoint.listen()
|
|
try {
|
|
await expect(probeAuthenticatedEndpoint({
|
|
socketPath, secret: Buffer.alloc(32, 8), ...endpointIdentity
|
|
})).rejects.toThrow('closed during handshake')
|
|
} finally { await endpoint.close() }
|
|
})
|
|
|
|
runOnUnix(
|
|
'never pre-unlinks an endpoint that may still be live',
|
|
async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const socketPath = resolve(root, 'agent.sock')
|
|
const existing = createServer()
|
|
await new Promise<void>((resolveListen, reject) => {
|
|
existing.once('error', reject)
|
|
existing.listen(socketPath, resolveListen)
|
|
})
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath,
|
|
peerIdentity: currentPeerIdentity(),
|
|
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
|
|
controllers: new ControllerRegistry(),
|
|
...endpointIdentity,
|
|
onAttach: vi.fn()
|
|
})
|
|
|
|
await expect(endpoint.listen()).rejects.toMatchObject({
|
|
code: 'EADDRINUSE'
|
|
})
|
|
const client = createConnection(socketPath)
|
|
await expect(onceConnected(client)).resolves.toBeUndefined()
|
|
client.destroy()
|
|
await new Promise<void>((resolveClose) =>
|
|
existing.close(() => resolveClose())
|
|
)
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'destroys an authenticated persistent socket during shutdown',
|
|
async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const secret = Buffer.alloc(32, 7)
|
|
const verifier = new InstallationChallengeVerifier(secret)
|
|
const attached = vi.fn()
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
peerIdentity: currentPeerIdentity(),
|
|
challenge: verifier,
|
|
controllers: new ControllerRegistry(),
|
|
...endpointIdentity,
|
|
onAttach: attached
|
|
})
|
|
await endpoint.listen()
|
|
const client = createConnection(resolve(root, 'agent.sock'))
|
|
await onceConnected(client)
|
|
const challenge = await readPacket(client)
|
|
const clientNonce = 'client-nonce'
|
|
await writePacket(client, {
|
|
type: 'response',
|
|
version: 1,
|
|
controllerId: 'controller-a',
|
|
clientNonce,
|
|
response: verifier.createResponse({
|
|
serverNonce: String(challenge.serverNonce),
|
|
clientNonce,
|
|
controllerId: 'controller-a'
|
|
}),
|
|
protocol: AGENT_PROTOCOL_VERSION
|
|
})
|
|
const welcome = await readPacket(client)
|
|
expect(attachWelcomeSchema.parse(welcome)).toEqual({
|
|
type: 'goodbuddy-agent-welcome',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
connectionId: expect.any(String),
|
|
generation: 1,
|
|
...endpointIdentity,
|
|
serverNonce: challenge.serverNonce
|
|
})
|
|
await vi.waitFor(() => expect(attached).toHaveBeenCalledOnce())
|
|
|
|
const closed = onceClosed(client)
|
|
await expect(endpoint.close()).resolves.toBeUndefined()
|
|
await closed
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'settles a socket that is still resolving peer identity',
|
|
async () => {
|
|
const root = privateTemporaryDirectory()
|
|
let resolvePeer: ((peer: PeerIdentity) => void) | undefined
|
|
const peerIdentity = vi.fn(
|
|
() =>
|
|
new Promise<PeerIdentity>((resolveIdentity) => {
|
|
resolvePeer = resolveIdentity
|
|
})
|
|
)
|
|
const attached = vi.fn()
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
peerIdentity: { getPeerIdentity: peerIdentity },
|
|
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
|
|
controllers: new ControllerRegistry(),
|
|
...endpointIdentity,
|
|
onAttach: attached
|
|
})
|
|
await endpoint.listen()
|
|
const client = createConnection(resolve(root, 'agent.sock'))
|
|
await onceConnected(client)
|
|
await vi.waitFor(() => expect(peerIdentity).toHaveBeenCalledOnce())
|
|
|
|
const closed = onceClosed(client)
|
|
await expect(endpoint.close()).resolves.toBeUndefined()
|
|
await closed
|
|
resolvePeer?.({ uid: process.getuid!() })
|
|
await Promise.resolve()
|
|
expect(attached).not.toHaveBeenCalled()
|
|
}
|
|
)
|
|
|
|
runOnUnix('rejects connections beyond the configured cap', async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const peerIdentity = vi.fn(
|
|
() => new Promise<PeerIdentity>(() => undefined)
|
|
)
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
peerIdentity: { getPeerIdentity: peerIdentity },
|
|
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
|
|
controllers: new ControllerRegistry(),
|
|
...endpointIdentity,
|
|
onAttach: vi.fn(),
|
|
maximumConnections: 1
|
|
})
|
|
await endpoint.listen()
|
|
const first = createConnection(resolve(root, 'agent.sock'))
|
|
await onceConnected(first)
|
|
await vi.waitFor(() => expect(peerIdentity).toHaveBeenCalledOnce())
|
|
|
|
const second = createConnection(resolve(root, 'agent.sock'))
|
|
const rejected = onceClosed(second)
|
|
await onceConnected(second)
|
|
await rejected
|
|
expect(peerIdentity).toHaveBeenCalledOnce()
|
|
|
|
await endpoint.close()
|
|
first.destroy()
|
|
})
|
|
|
|
runOnUnix(
|
|
'rejects malformed or client-extended authentication packets',
|
|
async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
peerIdentity: currentPeerIdentity(),
|
|
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
|
|
controllers: new ControllerRegistry(),
|
|
...endpointIdentity,
|
|
onAttach: vi.fn()
|
|
})
|
|
await endpoint.listen()
|
|
const client = createConnection(resolve(root, 'agent.sock'))
|
|
await onceConnected(client)
|
|
await readPacket(client)
|
|
await writePacket(client, {
|
|
type: 'response',
|
|
version: 1,
|
|
controllerId: 'controller-a',
|
|
clientNonce: 'client-nonce',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
response: 'not-authenticated',
|
|
installationId: 'client-claimed'
|
|
})
|
|
await onceClosed(client)
|
|
await endpoint.close()
|
|
}
|
|
)
|
|
|
|
runOnUnix(
|
|
'relays a strict Main attach into real protocol status and capabilities',
|
|
async () => {
|
|
const root = privateTemporaryDirectory()
|
|
const controllers = new ControllerRegistry()
|
|
const events = new EventJournal(resolve(root, 'events.sqlite'))
|
|
const status = {
|
|
state: 'ready' as const,
|
|
installationId: endpointIdentity.installationId,
|
|
binaryDigest,
|
|
daemonBootId: endpointIdentity.daemonBootId,
|
|
agentVersion: '0.11.0',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
platform: 'linux' as const,
|
|
architecture: 'x64' as const,
|
|
supervisor: 'detached-on-demand' as const,
|
|
remoteUserIdentity: `uid:${process.getuid!()}`,
|
|
draining: false
|
|
}
|
|
const readMethods = Object.fromEntries(
|
|
[
|
|
'workspace/validate',
|
|
'workspace/open',
|
|
'workspace/resume',
|
|
'workspace/close',
|
|
'workspace/list',
|
|
'workspace/stat',
|
|
'workspace/readText',
|
|
'workspace/search',
|
|
'git/status',
|
|
'git/diff'
|
|
].map((method) => [method, () => null])
|
|
)
|
|
const protocol = new AgentProtocolServer({
|
|
controllers,
|
|
events,
|
|
status: () => status,
|
|
methods: readMethods
|
|
})
|
|
const secret = Buffer.alloc(32, 7)
|
|
const endpoint = new PrivateEndpoint({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
peerIdentity: currentPeerIdentity(),
|
|
challenge: new InstallationChallengeVerifier(secret),
|
|
controllers,
|
|
...endpointIdentity,
|
|
onAttach: ({ socket, controller }) => {
|
|
protocol.accept(socket, controller)
|
|
}
|
|
})
|
|
await endpoint.listen()
|
|
const input = new PassThrough()
|
|
const output = new PassThrough()
|
|
const relay = attachRelay({
|
|
socketPath: resolve(root, 'agent.sock'),
|
|
secret,
|
|
input,
|
|
output
|
|
})
|
|
writeStreamPacket(input, {
|
|
type: 'goodbuddy-agent-attach',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
goodBuddyVersion: '0.11.0',
|
|
controllerId: 'controller-main',
|
|
clientNonce: 'main-client-nonce',
|
|
hostRevision: 2,
|
|
hostKeyGeneration: 3
|
|
})
|
|
const welcome = attachWelcomeSchema.parse(
|
|
await readStreamPacket(output)
|
|
)
|
|
expect(welcome).toMatchObject({
|
|
type: 'goodbuddy-agent-welcome',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
installationId: endpointIdentity.installationId,
|
|
binaryDigest,
|
|
daemonBootId: endpointIdentity.daemonBootId,
|
|
generation: 1
|
|
})
|
|
|
|
input.write(
|
|
controlRequest(welcome, 'status-channel', 1, 'agent/status')
|
|
)
|
|
const statusResponse = await readFrame(output)
|
|
await readFrame(output)
|
|
expect(
|
|
daemonStatusSchema.parse(jsonRpcResult(statusResponse))
|
|
).toEqual(status)
|
|
|
|
input.write(
|
|
controlRequest(
|
|
welcome,
|
|
'capabilities-channel',
|
|
2,
|
|
'agent/capabilities'
|
|
)
|
|
)
|
|
const capabilitiesResponse = await readFrame(output)
|
|
await readFrame(output)
|
|
expect(
|
|
daemonCapabilitiesSchema.parse(
|
|
jsonRpcResult(capabilitiesResponse)
|
|
)
|
|
).toEqual({
|
|
generation: welcome.generation,
|
|
capabilities: [
|
|
{ name: 'agent/control', version: 1, critical: true },
|
|
{ name: 'workspace/read', version: 1, critical: true }
|
|
],
|
|
runtimes: []
|
|
})
|
|
|
|
input.end()
|
|
await relay
|
|
await endpoint.close()
|
|
events.close()
|
|
},
|
|
30_000
|
|
)
|
|
})
|
|
|
|
describe('attach relay preface validation', () => {
|
|
it.each(['before', 'during'])('rejects a stream destroyed %s its preface arrives', async (when) => {
|
|
const input = new PassThrough()
|
|
if (when === 'before') input.destroy()
|
|
const relay = attachRelay({
|
|
socketPath: resolve('missing-agent.sock'), secret: Buffer.alloc(32, 7),
|
|
input, output: new PassThrough()
|
|
})
|
|
const rejection = expect(relay).rejects.toThrow('closed during preface')
|
|
if (when === 'during') input.destroy()
|
|
await rejection
|
|
expect(input.listenerCount('readable')).toBe(0)
|
|
expect(input.listenerCount('close')).toBe(0)
|
|
})
|
|
|
|
it('rejects unknown Main preface fields before opening an endpoint', async () => {
|
|
const input = new PassThrough()
|
|
const output = new PassThrough()
|
|
writeStreamPacket(input, {
|
|
type: 'goodbuddy-agent-attach',
|
|
protocol: AGENT_PROTOCOL_VERSION,
|
|
goodBuddyVersion: '0.11.0',
|
|
controllerId: 'controller-main',
|
|
clientNonce: 'main-client-nonce',
|
|
hostRevision: 2,
|
|
hostKeyGeneration: 3,
|
|
installationId: 'client-claimed'
|
|
})
|
|
await expect(
|
|
attachRelay({
|
|
socketPath: resolve('missing-agent.sock'),
|
|
secret: Buffer.alloc(32, 7),
|
|
input,
|
|
output
|
|
})
|
|
).rejects.toThrow()
|
|
})
|
|
|
|
it('rejects malformed and oversized Main prefaces deterministically', async () => {
|
|
const malformed = new PassThrough()
|
|
writeStreamPacket(malformed, {
|
|
type: 'goodbuddy-agent-attach',
|
|
protocol: { major: 1, minor: -1 }
|
|
})
|
|
await expect(
|
|
attachRelay({
|
|
socketPath: resolve('missing-agent.sock'),
|
|
secret: Buffer.alloc(32, 7),
|
|
input: malformed,
|
|
output: new PassThrough()
|
|
})
|
|
).rejects.toThrow()
|
|
|
|
const oversized = new PassThrough()
|
|
const length = Buffer.alloc(4)
|
|
length.writeUInt32BE(4097)
|
|
oversized.end(length)
|
|
await expect(
|
|
attachRelay({
|
|
socketPath: resolve('missing-agent.sock'),
|
|
secret: Buffer.alloc(32, 7),
|
|
input: oversized,
|
|
output: new PassThrough()
|
|
})
|
|
).rejects.toThrow('oversized')
|
|
})
|
|
})
|
|
|
|
function currentPeerIdentity(): {
|
|
getPeerIdentity(): Promise<PeerIdentity>
|
|
} {
|
|
return {
|
|
async getPeerIdentity() {
|
|
return { uid: process.getuid!() }
|
|
}
|
|
}
|
|
}
|
|
|
|
function privateTemporaryDirectory(): string {
|
|
const path = mkdtempSync(join(tmpdir(), 'goodbuddy-endpoint-'))
|
|
chmodSync(path, 0o700)
|
|
temporaryPaths.push(path)
|
|
return path
|
|
}
|
|
|
|
async function onceConnected(socket: Socket): Promise<void> {
|
|
await new Promise<void>((resolveConnection, reject) => {
|
|
socket.once('connect', resolveConnection)
|
|
socket.once('error', reject)
|
|
})
|
|
}
|
|
|
|
async function onceClosed(socket: Socket): Promise<void> {
|
|
await new Promise<void>((resolveClose) => {
|
|
socket.once('close', resolveClose)
|
|
})
|
|
}
|
|
|
|
async function readPacket(socket: Socket): Promise<Record<string, unknown>> {
|
|
const length = await readExactly(socket, 4)
|
|
const contents = await readExactly(socket, length.readUInt32BE(0))
|
|
return JSON.parse(contents.toString('utf8')) as Record<string, unknown>
|
|
}
|
|
|
|
async function writePacket(
|
|
socket: Socket,
|
|
value: Record<string, unknown>
|
|
): Promise<void> {
|
|
const contents = Buffer.from(JSON.stringify(value), 'utf8')
|
|
const packet = Buffer.allocUnsafe(contents.byteLength + 4)
|
|
packet.writeUInt32BE(contents.byteLength, 0)
|
|
contents.copy(packet, 4)
|
|
await new Promise<void>((resolveWrite, reject) => {
|
|
socket.write(packet, (error) => {
|
|
if (error === null || error === undefined) {
|
|
resolveWrite()
|
|
} else {
|
|
reject(error)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
async function readExactly(
|
|
socket: NodeJS.ReadableStream & {
|
|
read(size?: number): Buffer | string | null
|
|
},
|
|
byteLength: number
|
|
): Promise<Buffer> {
|
|
const chunks: Buffer[] = []
|
|
let remaining = byteLength
|
|
while (remaining > 0) {
|
|
const chunk = socket.read(remaining)
|
|
if (chunk !== null) {
|
|
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
chunks.push(bytes)
|
|
remaining -= bytes.byteLength
|
|
continue
|
|
}
|
|
await new Promise<void>((resolveRead, reject) => {
|
|
socket.once('readable', resolveRead)
|
|
socket.once('error', reject)
|
|
})
|
|
}
|
|
return Buffer.concat(chunks, byteLength)
|
|
}
|
|
|
|
function writeStreamPacket(
|
|
output: NodeJS.WritableStream,
|
|
value: Record<string, unknown>
|
|
): void {
|
|
const contents = Buffer.from(JSON.stringify(value), 'utf8')
|
|
const packet = Buffer.allocUnsafe(contents.byteLength + 4)
|
|
packet.writeUInt32BE(contents.byteLength, 0)
|
|
contents.copy(packet, 4)
|
|
output.write(packet)
|
|
}
|
|
|
|
async function readStreamPacket(
|
|
input: PassThrough
|
|
): Promise<Record<string, unknown>> {
|
|
const length = await readExactly(input, 4)
|
|
const contents = await readExactly(input, length.readUInt32BE(0))
|
|
return JSON.parse(contents.toString('utf8')) as Record<string, unknown>
|
|
}
|
|
|
|
function controlRequest(
|
|
welcome: {
|
|
connectionId: string
|
|
generation: number
|
|
protocol: { major: number; minor: number }
|
|
},
|
|
channelId: string,
|
|
id: number,
|
|
method: string
|
|
): Uint8Array {
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ jsonrpc: '2.0', id, method, params: {} }),
|
|
'utf8'
|
|
)
|
|
return encodeAgentFrame({
|
|
header: {
|
|
protocolMajor: welcome.protocol.major,
|
|
protocolMinor: welcome.protocol.minor,
|
|
connectionId: welcome.connectionId,
|
|
generation: welcome.generation,
|
|
channelId,
|
|
channelEpoch: '1',
|
|
direction: 'main-to-agent',
|
|
sequence: '1',
|
|
kind: 'control',
|
|
payloadLength: payload.byteLength
|
|
},
|
|
payload
|
|
})
|
|
}
|
|
|
|
async function readFrame(input: PassThrough): Promise<AgentFrame> {
|
|
const header = await readExactly(input, 40)
|
|
const connectionLength = header.readUInt16BE(32)
|
|
const channelLength = header.readUInt16BE(34)
|
|
const payloadLength = header.readUInt32BE(36)
|
|
const remainder = await readExactly(
|
|
input,
|
|
connectionLength + channelLength + payloadLength
|
|
)
|
|
return decodeAgentFrame(Buffer.concat([header, remainder]))
|
|
}
|
|
|
|
function jsonRpcResult(frame: AgentFrame): unknown {
|
|
const response = JSON.parse(
|
|
Buffer.from(frame.payload).toString('utf8')
|
|
) as { result?: unknown }
|
|
return response.result
|
|
}
|