feat: add managed SSH execution spaces

Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI.

Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts.

Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
This commit is contained in:
mesalogo
2026-08-24 23:02:18 +08:00
parent 3829a871f5
commit 607bb33249
258 changed files with 99449 additions and 2427 deletions
+128
View File
@@ -0,0 +1,128 @@
import {
operationIdentitySchema,
operationMethodSchema,
operationScopeSchema,
type OperationIdentity,
type OperationScope
} from './contracts'
type JsonPrimitive = boolean | null | number | string
export type CanonicalJsonValue =
| JsonPrimitive
| CanonicalJsonValue[]
| { [key: string]: CanonicalJsonValue }
export class CanonicalPayloadError extends Error {
constructor(message: string) {
super(message)
this.name = 'CanonicalPayloadError'
}
}
export function canonicalJson(value: unknown): string {
return serialize(value, new Set())
}
export async function digestCanonicalOperation(input: {
method: string
scope: OperationScope
payload: unknown
}): Promise<string> {
const scope = operationScopeSchema.parse(input.scope)
const method = operationMethodSchema.parse(input.method)
const canonical = canonicalJson({
method,
payload: input.payload,
scope
})
const digest = await globalThis.crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(canonical)
)
return `sha256:${Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, '0')
).join('')}`
}
export async function createOperationIdentity(input: {
controllerId: string
operationId: string
scope: OperationScope
method: string
payload: unknown
}): Promise<OperationIdentity> {
return operationIdentitySchema.parse({
controllerId: input.controllerId,
operationId: input.operationId,
scope: input.scope,
method: input.method,
payloadDigest: await digestCanonicalOperation(input)
})
}
function serialize(value: unknown, ancestors: Set<object>): string {
if (value === null || typeof value === 'boolean') {
return JSON.stringify(value)
}
if (typeof value === 'string') {
return JSON.stringify(value)
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) {
throw new CanonicalPayloadError('Non-finite numbers are not JSON')
}
return JSON.stringify(Object.is(value, -0) ? 0 : value)
}
if (
typeof value === 'bigint' ||
typeof value === 'function' ||
typeof value === 'symbol' ||
typeof value === 'undefined'
) {
throw new CanonicalPayloadError(
`Unsupported JSON value: ${typeof value}`
)
}
if (typeof value !== 'object') {
throw new CanonicalPayloadError('Unsupported JSON value')
}
if (ancestors.has(value)) {
throw new CanonicalPayloadError('Circular JSON value')
}
ancestors.add(value)
try {
if (Array.isArray(value)) {
return `[${value
.map((item) => {
if (item === undefined) {
throw new CanonicalPayloadError(
'Undefined array values are not allowed'
)
}
return serialize(item, ancestors)
})
.join(',')}]`
}
const prototype = Object.getPrototypeOf(value)
if (prototype !== Object.prototype && prototype !== null) {
throw new CanonicalPayloadError(
'Only plain JSON objects are allowed'
)
}
const record = value as Record<string, unknown>
const keys = Object.keys(record).sort()
return `{${keys
.map((key) => {
const item = record[key]
if (item === undefined) {
throw new CanonicalPayloadError(
'Undefined object values are not allowed'
)
}
return `${JSON.stringify(key)}:${serialize(item, ancestors)}`
})
.join(',')}}`
} finally {
ancestors.delete(value)
}
}
+293
View File
@@ -0,0 +1,293 @@
import {
AGENT_PROTOCOL_LIMITS,
agentFrameHeaderSchema,
agentIdentifierSchema,
positiveAgentSequenceSchema,
type AgentFrameHeader
} from './contracts'
export class ChannelProtocolError extends Error {
constructor(
message: string,
readonly code:
| 'identity-mismatch'
| 'generation-mismatch'
| 'epoch-mismatch'
| 'direction-mismatch'
| 'sequence-mismatch'
| 'closed'
| 'channel-limit'
| 'channel-lifetime-exhausted'
) {
super(message)
this.name = 'ChannelProtocolError'
}
}
export type ChannelStateOptions = {
connectionId: string
generation: number
channelId: string
channelEpoch: string
inboundDirection: AgentFrameHeader['direction']
nextInboundSequence?: string
nextOutboundSequence?: string
}
export class AgentChannelState {
readonly connectionId: string
readonly generation: number
readonly channelId: string
readonly channelEpoch: string
readonly inboundDirection: AgentFrameHeader['direction']
#nextInboundSequence: bigint
#nextOutboundSequence: bigint
#nextInboundAckSequence = 1n
#nextOutboundAckSequence = 1n
#closing = false
#closed = false
constructor(options: ChannelStateOptions) {
agentIdentifierSchema.parse(options.connectionId)
agentIdentifierSchema.parse(options.channelId)
positiveAgentSequenceSchema.parse(options.channelEpoch)
positiveAgentSequenceSchema.parse(options.nextInboundSequence ?? '1')
positiveAgentSequenceSchema.parse(options.nextOutboundSequence ?? '1')
if (
!Number.isSafeInteger(options.generation) ||
options.generation < 1 ||
options.generation > 0xffff_ffff
) {
throw new RangeError('Invalid connection generation')
}
this.connectionId = options.connectionId
this.generation = options.generation
this.channelId = options.channelId
this.channelEpoch = options.channelEpoch
this.inboundDirection = options.inboundDirection
this.#nextInboundSequence = BigInt(options.nextInboundSequence ?? '1')
this.#nextOutboundSequence = BigInt(options.nextOutboundSequence ?? '1')
}
get closed(): boolean {
return this.#closed
}
get closing(): boolean {
return this.#closing
}
acceptInbound(headerInput: AgentFrameHeader): void {
this.#assertInboundOpen()
let header: AgentFrameHeader
try {
header = agentFrameHeaderSchema.parse(headerInput)
this.#assertIdentity(header)
} catch (error) {
this.#closed = true
throw error
}
if (header.direction !== this.inboundDirection) {
this.#closed = true
throw new ChannelProtocolError(
'Frame direction does not match channel',
'direction-mismatch'
)
}
const expectedSequence =
header.kind === 'ack'
? this.#nextInboundAckSequence
: this.#nextInboundSequence
if (BigInt(header.sequence) !== expectedSequence) {
this.#closed = true
throw new ChannelProtocolError(
'Frame sequence is not the next expected value',
'sequence-mismatch'
)
}
if (header.kind === 'ack') {
this.#nextInboundAckSequence += 1n
} else {
this.#nextInboundSequence += 1n
}
}
reserveOutbound(
header: Omit<AgentFrameHeader, 'sequence'>
): AgentFrameHeader {
this.#assertOpen()
const sequence =
header.kind === 'ack'
? this.#nextOutboundAckSequence
: this.#nextOutboundSequence
const candidate = agentFrameHeaderSchema.parse({
...header,
sequence: sequence.toString()
})
this.#assertIdentity(candidate)
if (candidate.direction === this.inboundDirection) {
throw new ChannelProtocolError(
'Outbound direction matches inbound direction',
'direction-mismatch'
)
}
if (candidate.kind === 'ack') {
this.#nextOutboundAckSequence += 1n
} else {
this.#nextOutboundSequence += 1n
}
return candidate
}
beginClose(): void {
this.#assertOpen()
this.#closing = true
}
close(): void {
this.#closing = true
this.#closed = true
}
#assertInboundOpen(): void {
this.#assertOpen()
if (this.#closing) {
this.#closed = true
throw new ChannelProtocolError('Channel is closing', 'closed')
}
}
#assertOpen(): void {
if (this.#closed) {
throw new ChannelProtocolError('Channel is closed', 'closed')
}
}
#assertIdentity(header: AgentFrameHeader): void {
if (
header.connectionId !== this.connectionId ||
header.channelId !== this.channelId
) {
throw new ChannelProtocolError(
'Frame identity does not match channel',
'identity-mismatch'
)
}
if (header.generation !== this.generation) {
throw new ChannelProtocolError(
'Frame belongs to an obsolete connection generation',
'generation-mismatch'
)
}
if (header.channelEpoch !== this.channelEpoch) {
throw new ChannelProtocolError(
'Frame belongs to another channel epoch',
'epoch-mismatch'
)
}
}
}
export class AgentConnectionChannels {
readonly #channels = new Map<string, AgentChannelState>()
readonly #retiredChannelIds = new Set<string>()
readonly #connectionId: string
readonly #generation: number
readonly #maximumChannels: number
readonly #maximumLifetimeChannels: number
#openedChannels = 0
constructor(options: {
connectionId: string
generation: number
maximumChannels?: number
maximumLifetimeChannels?: number
}) {
agentIdentifierSchema.parse(options.connectionId)
if (
!Number.isSafeInteger(options.generation) ||
options.generation < 1 ||
options.generation > 0xffff_ffff
) {
throw new RangeError('Invalid connection generation')
}
if (
options.maximumChannels !== undefined &&
(!Number.isSafeInteger(options.maximumChannels) ||
options.maximumChannels < 1 ||
options.maximumChannels >
AGENT_PROTOCOL_LIMITS.maximumChannelsPerConnection)
) {
throw new RangeError('Invalid connection channel limit')
}
const maximumChannels =
options.maximumChannels ??
AGENT_PROTOCOL_LIMITS.maximumChannelsPerConnection
const maximumLifetimeChannels =
options.maximumLifetimeChannels ??
AGENT_PROTOCOL_LIMITS.maximumChannelsPerConnection * 64
if (
!Number.isSafeInteger(maximumLifetimeChannels) ||
maximumLifetimeChannels < maximumChannels ||
maximumLifetimeChannels >
AGENT_PROTOCOL_LIMITS.maximumChannelsPerConnection * 64
) {
throw new RangeError('Invalid connection channel lifetime limit')
}
this.#connectionId = options.connectionId
this.#generation = options.generation
this.#maximumChannels = maximumChannels
this.#maximumLifetimeChannels = maximumLifetimeChannels
}
open(
options: Omit<
ChannelStateOptions,
'connectionId' | 'generation'
>
): AgentChannelState {
if (
this.#channels.has(options.channelId) ||
this.#retiredChannelIds.has(options.channelId)
) {
throw new ChannelProtocolError(
'Channel ID cannot be reused in a connection',
'identity-mismatch'
)
}
if (this.#openedChannels >= this.#maximumLifetimeChannels) {
throw new ChannelProtocolError(
'Connection channel lifetime is exhausted',
'channel-lifetime-exhausted'
)
}
if (this.#channels.size >= this.#maximumChannels) {
throw new ChannelProtocolError(
'Connection channel limit reached',
'channel-limit'
)
}
const channel = new AgentChannelState({
...options,
connectionId: this.#connectionId,
generation: this.#generation
})
this.#channels.set(options.channelId, channel)
this.#openedChannels += 1
return channel
}
get(channelId: string): AgentChannelState | undefined {
return this.#channels.get(channelId)
}
close(channelId: string): void {
const channel = this.#channels.get(channelId)
if (channel === undefined) {
return
}
channel.close()
this.#channels.delete(channelId)
this.#retiredChannelIds.add(channelId)
}
}
+247
View File
@@ -0,0 +1,247 @@
import { z } from 'zod'
export const AGENT_PROTOCOL_VERSION = {
major: 2,
minor: 0
} as const
export const AGENT_PROTOCOL_LIMITS = {
maximumControlFrameBytes: 1024 * 1024,
maximumAcpFrameBytes: 1024 * 1024,
maximumBlobFrameBytes: 2 * 1024 * 1024,
maximumAckFrameBytes: 1024,
maximumBufferedProtocolBytes: 32 * 1024 * 1024,
maximumChannelsPerConnection: 64,
maximumIdentifierBytes: 128,
maximumMethodLength: 200,
maximumEventPayloadBytes: 1024 * 1024,
runEventJournalBytes: 8 * 1024 * 1024,
runTerminalReserveBytes: 512 * 1024,
controllerJournalBytes: 128 * 1024 * 1024,
daemonJournalBytes: 512 * 1024 * 1024,
runPendingEvents: 1_000,
maximumAcpFramesPerChannel: 1_000,
maximumAcpFramesPerController: 32_000,
maximumAcpFramesPerDaemon: 128_000
} as const
const identifierPattern = /^[A-Za-z0-9](?:[A-Za-z0-9._:-]{0,127})$/u
const decimalPattern = /^(?:0|[1-9][0-9]{0,15})$/u
export const agentIdentifierSchema = z
.string()
.min(1)
.max(AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes)
.regex(identifierPattern)
export const agentSequenceSchema = z
.string()
.regex(decimalPattern)
.refine(
(value) => BigInt(value) <= BigInt(Number.MAX_SAFE_INTEGER),
'Sequence exceeds the process-neutral safe integer range'
)
export const positiveAgentSequenceSchema = agentSequenceSchema.refine(
(value) => value !== '0',
'Sequence must be positive'
)
export const protocolVersionSchema = z
.object({
major: z.number().int().min(0).max(65_535),
minor: z.number().int().min(0).max(65_535)
})
.strict()
export const agentFrameKindSchema = z.enum([
'control',
'acp',
'blob',
'ack'
])
export const agentFrameDirectionSchema = z.enum([
'main-to-agent',
'agent-to-main'
])
export const agentFrameHeaderSchema = z
.object({
protocolMajor: protocolVersionSchema.shape.major,
protocolMinor: protocolVersionSchema.shape.minor,
connectionId: agentIdentifierSchema,
generation: z.number().int().min(1).max(0xffff_ffff),
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
direction: agentFrameDirectionSchema,
sequence: positiveAgentSequenceSchema,
kind: agentFrameKindSchema,
payloadLength: z.number().int().min(0)
})
.strict()
.superRefine((header, context) => {
const maximum = maximumPayloadLength(header.kind)
if (header.payloadLength > maximum) {
context.addIssue({
code: 'too_big',
origin: 'number',
maximum,
inclusive: true,
path: ['payloadLength'],
message: `${header.kind} payload exceeds ${maximum} bytes`
})
}
})
export type AgentFrameKind = z.infer<typeof agentFrameKindSchema>
export type AgentFrameDirection = z.infer<
typeof agentFrameDirectionSchema
>
export type AgentFrameHeader = z.infer<typeof agentFrameHeaderSchema>
export const frameAcknowledgmentSchema = z
.object({
acknowledgedSequence: positiveAgentSequenceSchema
})
.strict()
export type FrameAcknowledgment = z.infer<
typeof frameAcknowledgmentSchema
>
export const operationScopeSchema = z.discriminatedUnion('kind', [
z
.object({
kind: z.literal('project-candidate'),
candidateId: agentIdentifierSchema
})
.strict(),
z
.object({
kind: z.literal('workspace'),
workspaceIdentity: agentIdentifierSchema
})
.strict(),
z
.object({
kind: z.literal('run'),
sessionId: agentIdentifierSchema,
requestId: agentIdentifierSchema
})
.strict(),
z
.object({
kind: z.literal('terminal'),
terminalId: agentIdentifierSchema
})
.strict(),
z
.object({
kind: z.literal('managed-process'),
processId: agentIdentifierSchema
})
.strict(),
z
.object({
kind: z.literal('installation'),
installationId: agentIdentifierSchema
})
.strict()
])
export type OperationScope = z.infer<typeof operationScopeSchema>
export const sha256DigestSchema = z
.string()
.regex(/^sha256:[a-f0-9]{64}$/u)
export function utf8ByteLength(value: string): number {
return new TextEncoder().encode(value).byteLength
}
export function utf8StringSchema(
maximumBytes: number,
options: {
minimumBytes?: number
label?: string
} = {}
): z.ZodType<string> {
if (
!Number.isSafeInteger(maximumBytes) ||
maximumBytes < 0 ||
!Number.isSafeInteger(options.minimumBytes ?? 0) ||
(options.minimumBytes ?? 0) < 0 ||
(options.minimumBytes ?? 0) > maximumBytes
) {
throw new RangeError('Invalid UTF-8 string byte limits')
}
const label = options.label ?? 'String'
const minimumBytes = options.minimumBytes ?? 0
return z.string().superRefine((value, context) => {
const bytes = utf8ByteLength(value)
if (bytes < minimumBytes) {
context.addIssue({
code: 'custom',
message: `${label} must contain at least ${minimumBytes} UTF-8 bytes`
})
}
if (bytes > maximumBytes) {
context.addIssue({
code: 'custom',
message: `${label} exceeds ${maximumBytes} UTF-8 bytes`
})
}
})
}
export const operationMethodSchema = z
.string()
.min(1)
.max(AGENT_PROTOCOL_LIMITS.maximumMethodLength)
.regex(/^[a-z][A-Za-z0-9-]*\/[A-Za-z][A-Za-z0-9-]*$/u)
export const operationIdentitySchema = z
.object({
controllerId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
scope: operationScopeSchema,
method: operationMethodSchema,
payloadDigest: sha256DigestSchema
})
.strict()
export type OperationIdentity = z.infer<typeof operationIdentitySchema>
export const eventIdentitySchema = z
.object({
controllerId: agentIdentifierSchema,
streamId: agentIdentifierSchema,
streamEpoch: positiveAgentSequenceSchema,
sequence: positiveAgentSequenceSchema
})
.strict()
export type EventIdentity = z.infer<typeof eventIdentitySchema>
export const acpFrameDirectionSchema = z.enum([
'main-to-runtime',
'runtime-to-main'
])
export type AcpFrameDirection = z.infer<
typeof acpFrameDirectionSchema
>
export function maximumPayloadLength(kind: AgentFrameKind): number {
switch (kind) {
case 'control':
return AGENT_PROTOCOL_LIMITS.maximumControlFrameBytes
case 'acp':
return AGENT_PROTOCOL_LIMITS.maximumAcpFrameBytes
case 'blob':
return AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes
case 'ack':
return AGENT_PROTOCOL_LIMITS.maximumAckFrameBytes
}
}
@@ -0,0 +1,521 @@
import { z } from 'zod'
import {
AGENT_PROTOCOL_LIMITS,
agentIdentifierSchema,
agentSequenceSchema,
positiveAgentSequenceSchema,
protocolVersionSchema,
sha256DigestSchema,
utf8StringSchema
} from './contracts'
const generationSchema = z.number().int().min(1).max(0xffff_ffff)
const timestampSchema = z.string().datetime({ offset: true })
export const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>
z.union([
z.null(),
z.boolean(),
z.number().finite(),
z.string(),
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema)
])
)
export type JsonValue =
| null
| boolean
| number
| string
| JsonValue[]
| { [key: string]: JsonValue }
export const jsonRpcIdSchema = z.union([
z.string().min(1).max(AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes),
z.number().int().safe()
])
export const jsonRpcResponseIdSchema = z.union([
jsonRpcIdSchema,
z.null()
])
export const jsonRpcParamsSchema = z.union([
z.array(jsonValueSchema),
z.record(z.string(), jsonValueSchema)
])
const jsonRpcMethodSchema = utf8StringSchema(
AGENT_PROTOCOL_LIMITS.maximumMethodLength,
{ minimumBytes: 1, label: 'JSON-RPC method' }
)
export const jsonRpcRequestSchema = z
.object({
jsonrpc: z.literal('2.0'),
id: jsonRpcIdSchema,
method: jsonRpcMethodSchema,
params: jsonRpcParamsSchema.optional()
})
.strict()
export const jsonRpcNotificationSchema = z
.object({
jsonrpc: z.literal('2.0'),
method: jsonRpcMethodSchema,
params: jsonRpcParamsSchema.optional()
})
.strict()
export const jsonRpcSuccessResponseSchema = z
.object({
jsonrpc: z.literal('2.0'),
id: jsonRpcResponseIdSchema,
result: jsonValueSchema
})
.strict()
export const jsonRpcErrorObjectSchema = z
.object({
code: z.number().int().safe(),
message: utf8StringSchema(8 * 1024, {
minimumBytes: 1,
label: 'JSON-RPC error message'
}),
data: jsonValueSchema.optional()
})
.strict()
export const jsonRpcErrorResponseSchema = z
.object({
jsonrpc: z.literal('2.0'),
id: jsonRpcResponseIdSchema,
error: jsonRpcErrorObjectSchema
})
.strict()
export const jsonRpcMessageSchema = z.union([
jsonRpcRequestSchema,
jsonRpcNotificationSchema,
jsonRpcSuccessResponseSchema,
jsonRpcErrorResponseSchema
])
export type JsonRpcRequest = z.infer<typeof jsonRpcRequestSchema>
export type JsonRpcNotification = z.infer<
typeof jsonRpcNotificationSchema
>
export type JsonRpcSuccessResponse = z.infer<
typeof jsonRpcSuccessResponseSchema
>
export type JsonRpcErrorResponse = z.infer<
typeof jsonRpcErrorResponseSchema
>
export type JsonRpcMessage = z.infer<typeof jsonRpcMessageSchema>
export const attachPrefaceSchema = z
.object({
type: z.literal('goodbuddy-agent-attach'),
protocol: protocolVersionSchema,
goodBuddyVersion: utf8StringSchema(64, {
minimumBytes: 1,
label: 'GoodBuddy version'
}),
controllerId: agentIdentifierSchema,
clientNonce: agentIdentifierSchema,
hostRevision: generationSchema,
hostKeyGeneration: generationSchema
})
.strict()
export const attachWelcomeSchema = z
.object({
type: z.literal('goodbuddy-agent-welcome'),
protocol: protocolVersionSchema,
connectionId: agentIdentifierSchema,
generation: generationSchema,
installationId: agentIdentifierSchema,
binaryDigest: sha256DigestSchema,
daemonBootId: agentIdentifierSchema,
serverNonce: agentIdentifierSchema
})
.strict()
export type AttachPreface = z.infer<typeof attachPrefaceSchema>
export type AttachWelcome = z.infer<typeof attachWelcomeSchema>
export const agentCapabilitySchema = z
.object({
name: z
.string()
.min(1)
.max(128)
.regex(/^[a-z][a-z0-9.-]*(?:\/[a-z][a-z0-9.-]*)?$/u),
version: z.number().int().min(1).max(65_535),
critical: z.boolean()
})
.strict()
export const daemonStatusSchema = z
.object({
state: z.enum([
'unknown',
'inspecting',
'missing',
'installed',
'installing',
'starting',
'handshaking',
'synchronizing',
'ready',
'draining',
'offline',
'incompatible',
'corrupt',
'supervisor-unavailable',
'upgrade-rollback'
]),
installationId: agentIdentifierSchema,
binaryDigest: sha256DigestSchema,
daemonBootId: agentIdentifierSchema,
agentVersion: utf8StringSchema(64, {
minimumBytes: 1,
label: 'Agent version'
}),
protocol: protocolVersionSchema,
platform: z.literal('linux'),
architecture: z.enum(['x64', 'arm64']),
supervisor: z.literal('detached-on-demand'),
remoteUserIdentity: agentIdentifierSchema,
draining: z.boolean()
})
.strict()
export const daemonCapabilitiesSchema = z
.object({
generation: generationSchema,
capabilities: z.array(agentCapabilitySchema).max(256),
runtimes: z
.array(
z
.object({
runtimeId: agentIdentifierSchema,
version: utf8StringSchema(64, {
minimumBytes: 1,
label: 'Runtime version'
}),
bundleDigest: sha256DigestSchema,
acpCapabilitiesDigest: sha256DigestSchema,
sessionLoad: z.boolean(),
sessionResume: z.boolean()
})
.strict()
)
.max(64)
})
.strict()
.superRefine((value, context) => {
const names = new Set<string>()
for (let index = 0; index < value.capabilities.length; index += 1) {
const name = value.capabilities[index]!.name
if (names.has(name)) {
context.addIssue({
code: 'custom',
path: ['capabilities', index, 'name'],
message: 'Capability names must be unique'
})
}
names.add(name)
}
})
export const controllerResumeRequestSchema = z
.object({
previousGeneration: generationSchema,
previousConnectionId: agentIdentifierSchema,
daemonBootId: agentIdentifierSchema,
capabilityGeneration: generationSchema
})
.strict()
export const controllerResumeResultSchema = z
.object({
resumed: z.boolean(),
generation: generationSchema,
daemonBootId: agentIdentifierSchema,
capabilityGeneration: generationSchema,
leaseDeadlineAt: timestampSchema
})
.strict()
export const acpOpenChannelRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
runtimeBundleDigest: sha256DigestSchema,
workspaceIdentity: agentIdentifierSchema
})
.strict()
export const acpOpenChannelResultSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
acpCapabilitiesDigest: sha256DigestSchema
})
.strict()
export const acpCloseChannelRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
reason: z.enum(['released', 'cancelled', 'draining', 'failed'])
})
.strict()
export const acpCloseChannelResultSchema = z
.object({
bindingId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
closed: z.literal(true)
})
.strict()
export const acpGetCursorsRequestSchema = z
.object({
bindingId: agentIdentifierSchema
})
.strict()
export const acpResumeChannelRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema
})
.strict()
export const acpBindingCursorsSchema = z
.object({
lastOutboundJournaledSequence: agentSequenceSchema,
lastOutboundDeliveredSequence: agentSequenceSchema,
lastInboundJournaledSequence: agentSequenceSchema,
lastMainAckSequence: agentSequenceSchema
})
.strict()
.superRefine((cursors, context) => {
if (
BigInt(cursors.lastOutboundDeliveredSequence) >
BigInt(cursors.lastOutboundJournaledSequence)
) {
context.addIssue({
code: 'custom',
path: ['lastOutboundDeliveredSequence'],
message: 'Delivered cursor cannot exceed journaled cursor'
})
}
if (
BigInt(cursors.lastMainAckSequence) >
BigInt(cursors.lastInboundJournaledSequence)
) {
context.addIssue({
code: 'custom',
path: ['lastMainAckSequence'],
message: 'Main ACK cursor cannot exceed journaled cursor'
})
}
})
export const acpResumeChannelResultSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
deadlineAt: timestampSchema,
cursors: acpBindingCursorsSchema
})
.strict()
export const acpReplayChannelRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
acknowledgedSequence: agentSequenceSchema
})
.strict()
export const acpReplayChannelResultSchema = z
.object({
bindingId: agentIdentifierSchema,
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema,
replayedThroughSequence: agentSequenceSchema,
live: z.literal(true)
})
.strict()
export const acpCancellationReasonSchema = z.enum([
'aborted',
'timeout',
'error',
'requested',
'unspecified'
])
export const acpEscalateCancellationRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
sessionId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
reason: acpCancellationReasonSchema
})
.strict()
export const acpEscalateCancellationResultSchema = z
.object({
bindingId: agentIdentifierSchema,
stopped: z.literal(true)
})
.strict()
export const acpReconcilePromptRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema
})
.strict()
export const acpCompletePromptRequestSchema = z
.object({
bindingId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema
})
.strict()
export const acpCompletePromptResultSchema = z
.object({
bindingId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
status: z.literal('completed'),
processTree: z.enum(['running', 'empty'])
})
.strict()
export const acpReconcilePromptResultSchema = z.discriminatedUnion(
'status',
[
z
.object({
status: z.literal('terminal'),
terminalState: z.enum([
'completed',
'failed',
'cancelled',
'interrupted'
]),
processTree: z.enum(['running', 'empty'])
})
.strict()
.superRefine((result, context) => {
if (
result.processTree === 'running' &&
result.terminalState !== 'completed'
) {
context.addIssue({
code: 'custom',
path: ['processTree'],
message:
'Only a completed prompt may retain a running process tree'
})
}
}),
z
.object({
status: z.literal('running'),
processTree: z.literal('running')
})
.strict(),
z
.object({
status: z.literal('outcome-unknown'),
processTree: z.literal('unknown')
})
.strict()
]
)
export const channelCloseRequestSchema = z
.object({
channelId: agentIdentifierSchema,
channelEpoch: positiveAgentSequenceSchema
})
.strict()
export type DaemonStatus = z.infer<typeof daemonStatusSchema>
export type DaemonCapabilities = z.infer<typeof daemonCapabilitiesSchema>
export type ControllerResumeRequest = z.infer<
typeof controllerResumeRequestSchema
>
export type ControllerResumeResult = z.infer<
typeof controllerResumeResultSchema
>
export type AcpOpenChannelRequest = z.infer<
typeof acpOpenChannelRequestSchema
>
export type AcpOpenChannelResult = z.infer<
typeof acpOpenChannelResultSchema
>
export type AcpCloseChannelRequest = z.infer<
typeof acpCloseChannelRequestSchema
>
export type AcpCloseChannelResult = z.infer<
typeof acpCloseChannelResultSchema
>
export type AcpGetCursorsRequest = z.infer<
typeof acpGetCursorsRequestSchema
>
export type AcpResumeChannelRequest = z.infer<
typeof acpResumeChannelRequestSchema
>
export type AcpResumeChannelResult = z.infer<
typeof acpResumeChannelResultSchema
>
export type AcpReplayChannelRequest = z.infer<
typeof acpReplayChannelRequestSchema
>
export type AcpReplayChannelResult = z.infer<
typeof acpReplayChannelResultSchema
>
export type AcpBindingCursors = z.infer<
typeof acpBindingCursorsSchema
>
export type AcpEscalateCancellationRequest = z.infer<
typeof acpEscalateCancellationRequestSchema
>
export type AcpEscalateCancellationResult = z.infer<
typeof acpEscalateCancellationResultSchema
>
export type AcpReconcilePromptRequest = z.infer<
typeof acpReconcilePromptRequestSchema
>
export type AcpReconcilePromptResult = z.infer<
typeof acpReconcilePromptResultSchema
>
export type AcpCompletePromptRequest = z.infer<
typeof acpCompletePromptRequestSchema
>
export type AcpCompletePromptResult = z.infer<
typeof acpCompletePromptResultSchema
>
export const agentAttachPrefaceSchema = attachPrefaceSchema
export const agentAttachWelcomeSchema = attachWelcomeSchema
export const agentStatusSchema = daemonStatusSchema
export const agentCapabilitiesSchema = daemonCapabilitiesSchema
@@ -0,0 +1,26 @@
import { z } from 'zod'
import { agentIdentifierSchema } from './contracts'
export const AGENT_PROTOCOL_FAILURE_RECORD_NAME =
'protocol-failure.json'
export const AGENT_PROTOCOL_FAILURE_STDERR_PREFIX =
'GoodBuddy Agent protocol failure: '
export const agentProtocolFailureCategorySchema = z
.string()
.min(1)
.max(128)
.regex(/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/u)
export const agentProtocolFailureRecordSchema = z
.object({
formatVersion: z.literal(1),
connectionId: agentIdentifierSchema,
category: agentProtocolFailureCategorySchema,
createdAt: z.number().int().safe().nonnegative()
})
.strict()
export type AgentProtocolFailureCategory = z.infer<
typeof agentProtocolFailureCategorySchema
>
+286
View File
@@ -0,0 +1,286 @@
import {
AGENT_PROTOCOL_LIMITS,
agentFrameHeaderSchema,
type AgentFrameHeader,
type AgentFrameKind,
maximumPayloadLength
} from './contracts'
const MAGIC = new Uint8Array([0x47, 0x42, 0x41, 0x31])
const ENVELOPE_VERSION = 1
export const AGENT_FRAME_FIXED_HEADER_BYTES = 40
const FIXED_HEADER_BYTES = AGENT_FRAME_FIXED_HEADER_BYTES
export const MAXIMUM_ENCODED_AGENT_FRAME_BYTES =
AGENT_FRAME_FIXED_HEADER_BYTES +
AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes * 2 +
Math.max(
AGENT_PROTOCOL_LIMITS.maximumControlFrameBytes,
AGENT_PROTOCOL_LIMITS.maximumAcpFrameBytes,
AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes,
AGENT_PROTOCOL_LIMITS.maximumAckFrameBytes
)
const kindCodes = {
control: 0,
acp: 1,
blob: 2,
ack: 3
} as const satisfies Record<AgentFrameKind, number>
const kinds = [
'control',
'acp',
'blob',
'ack'
] as const satisfies readonly AgentFrameKind[]
export type AgentFrame = {
header: AgentFrameHeader
payload: Uint8Array
}
export class AgentFrameError extends Error {
constructor(
message: string,
readonly code:
| 'incomplete'
| 'invalid'
| 'unsupported'
| 'oversized'
) {
super(message)
this.name = 'AgentFrameError'
}
}
export function inspectAgentFramePrefix(bytes: Uint8Array): {
kind: AgentFrameKind
encodedByteLength: number
} {
if (bytes.byteLength < FIXED_HEADER_BYTES) {
throw new AgentFrameError('Incomplete fixed frame header', 'incomplete')
}
for (let index = 0; index < MAGIC.byteLength; index += 1) {
if (bytes[index] !== MAGIC[index]) {
throw new AgentFrameError('Invalid frame magic', 'invalid')
}
}
const view = new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
)
if (view.getUint8(4) !== ENVELOPE_VERSION) {
throw new AgentFrameError(
'Unsupported envelope version',
'unsupported'
)
}
if (view.getUint8(7) !== 0) {
throw new AgentFrameError('Reserved frame flags are non-zero', 'invalid')
}
const kind = kinds[view.getUint8(5)]
if (kind === undefined) {
throw new AgentFrameError('Unknown frame kind', 'unsupported')
}
if (view.getUint8(6) > 1) {
throw new AgentFrameError('Unknown frame direction', 'invalid')
}
const connectionLength = view.getUint16(32)
const channelLength = view.getUint16(34)
if (
connectionLength === 0 ||
channelLength === 0 ||
connectionLength > AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes ||
channelLength > AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes
) {
throw new AgentFrameError('Invalid frame identifier length', 'invalid')
}
const payloadLength = view.getUint32(36)
if (payloadLength > maximumPayloadLength(kind)) {
throw new AgentFrameError('Frame payload exceeds its limit', 'oversized')
}
return {
kind,
encodedByteLength:
FIXED_HEADER_BYTES +
connectionLength +
channelLength +
payloadLength
}
}
export function encodeAgentFrame(frame: AgentFrame): Uint8Array {
const header = agentFrameHeaderSchema.parse({
...frame.header,
payloadLength: frame.payload.byteLength
})
const connection = encodeIdentifier(header.connectionId)
const channel = encodeIdentifier(header.channelId)
const output = new Uint8Array(
FIXED_HEADER_BYTES +
connection.byteLength +
channel.byteLength +
frame.payload.byteLength
)
output.set(MAGIC, 0)
const view = new DataView(output.buffer)
view.setUint8(4, ENVELOPE_VERSION)
view.setUint8(5, kindCodes[header.kind])
view.setUint8(6, header.direction === 'main-to-agent' ? 0 : 1)
view.setUint8(7, 0)
view.setUint16(8, header.protocolMajor)
view.setUint16(10, header.protocolMinor)
view.setUint32(12, header.generation)
view.setBigUint64(16, BigInt(header.channelEpoch))
view.setBigUint64(24, BigInt(header.sequence))
view.setUint16(32, connection.byteLength)
view.setUint16(34, channel.byteLength)
view.setUint32(36, frame.payload.byteLength)
output.set(connection, FIXED_HEADER_BYTES)
output.set(channel, FIXED_HEADER_BYTES + connection.byteLength)
output.set(
frame.payload,
FIXED_HEADER_BYTES + connection.byteLength + channel.byteLength
)
return output
}
export function decodeAgentFrame(
bytes: Uint8Array,
expected?: {
protocolMajor?: number
maximumProtocolMinor?: number
connectionId?: string
generation?: number
}
): AgentFrame {
if (bytes.byteLength < FIXED_HEADER_BYTES) {
throw new AgentFrameError('Incomplete fixed frame header', 'incomplete')
}
for (let index = 0; index < MAGIC.byteLength; index += 1) {
if (bytes[index] !== MAGIC[index]) {
throw new AgentFrameError('Invalid frame magic', 'invalid')
}
}
const view = new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
)
if (view.getUint8(4) !== ENVELOPE_VERSION) {
throw new AgentFrameError(
'Unsupported envelope version',
'unsupported'
)
}
if (view.getUint8(7) !== 0) {
throw new AgentFrameError('Reserved frame flags are non-zero', 'invalid')
}
const kind = kinds[view.getUint8(5)]
if (kind === undefined) {
throw new AgentFrameError('Unknown frame kind', 'unsupported')
}
const directionCode = view.getUint8(6)
if (directionCode > 1) {
throw new AgentFrameError('Unknown frame direction', 'invalid')
}
const connectionLength = view.getUint16(32)
const channelLength = view.getUint16(34)
if (
connectionLength === 0 ||
channelLength === 0 ||
connectionLength > AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes ||
channelLength > AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes
) {
throw new AgentFrameError('Invalid frame identifier length', 'invalid')
}
const payloadLength = view.getUint32(36)
if (payloadLength > maximumPayloadLength(kind)) {
throw new AgentFrameError('Frame payload exceeds its limit', 'oversized')
}
const expectedLength =
FIXED_HEADER_BYTES +
connectionLength +
channelLength +
payloadLength
if (bytes.byteLength < expectedLength) {
throw new AgentFrameError('Incomplete frame payload', 'incomplete')
}
if (bytes.byteLength !== expectedLength) {
throw new AgentFrameError('Trailing bytes after frame', 'invalid')
}
const decoder = new TextDecoder('utf-8', { fatal: true })
let connectionId: string
let channelId: string
try {
connectionId = decoder.decode(
bytes.subarray(
FIXED_HEADER_BYTES,
FIXED_HEADER_BYTES + connectionLength
)
)
channelId = decoder.decode(
bytes.subarray(
FIXED_HEADER_BYTES + connectionLength,
FIXED_HEADER_BYTES + connectionLength + channelLength
)
)
} catch {
throw new AgentFrameError('Invalid UTF-8 frame identifier', 'invalid')
}
const header = agentFrameHeaderSchema.parse({
protocolMajor: view.getUint16(8),
protocolMinor: view.getUint16(10),
connectionId,
generation: view.getUint32(12),
channelId,
channelEpoch: view.getBigUint64(16).toString(),
direction:
directionCode === 0 ? 'main-to-agent' : 'agent-to-main',
sequence: view.getBigUint64(24).toString(),
kind,
payloadLength
})
if (
expected?.protocolMajor !== undefined &&
header.protocolMajor !== expected.protocolMajor
) {
throw new AgentFrameError('Protocol major mismatch', 'unsupported')
}
if (
expected?.maximumProtocolMinor !== undefined &&
header.protocolMinor > expected.maximumProtocolMinor
) {
throw new AgentFrameError('Protocol minor was not negotiated', 'unsupported')
}
if (
expected?.connectionId !== undefined &&
header.connectionId !== expected.connectionId
) {
throw new AgentFrameError('Connection identity mismatch', 'invalid')
}
if (
expected?.generation !== undefined &&
header.generation !== expected.generation
) {
throw new AgentFrameError('Connection generation mismatch', 'invalid')
}
const payloadOffset =
FIXED_HEADER_BYTES + connectionLength + channelLength
return {
header,
payload: bytes.slice(payloadOffset, expectedLength)
}
}
function encodeIdentifier(value: string): Uint8Array {
const encoded = new TextEncoder().encode(value)
if (
encoded.byteLength === 0 ||
encoded.byteLength > AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes
) {
throw new AgentFrameError('Frame identifier is oversized', 'oversized')
}
return encoded
}
+6
View File
@@ -0,0 +1,6 @@
export * from './canonical'
export * from './channel-state'
export * from './contracts'
export * from './control-contracts'
export * from './frame'
export * from './failure-diagnostic'
+402
View File
@@ -0,0 +1,402 @@
import { describe, expect, it } from 'vitest'
import {
AGENT_PROTOCOL_LIMITS,
AGENT_PROTOCOL_VERSION,
AgentChannelState,
CanonicalPayloadError,
MAXIMUM_ENCODED_AGENT_FRAME_BYTES,
acpReplayChannelRequestSchema,
acpResumeChannelResultSchema,
agentFrameHeaderSchema,
attachPrefaceSchema,
canonicalJson,
controllerResumeRequestSchema,
controllerResumeResultSchema,
decodeAgentFrame,
daemonStatusSchema,
daemonCapabilitiesSchema,
digestCanonicalOperation,
encodeAgentFrame,
jsonRpcMessageSchema,
operationIdentitySchema,
type AgentFrameHeader
} from '.'
const header: AgentFrameHeader = {
protocolMajor: AGENT_PROTOCOL_VERSION.major,
protocolMinor: AGENT_PROTOCOL_VERSION.minor,
connectionId: 'connection-1',
generation: 3,
channelId: 'control-1',
channelEpoch: '7',
direction: 'main-to-agent',
sequence: '42',
kind: 'control',
payloadLength: 0
}
describe('agent protocol contracts', () => {
it('strictly rejects unknown operation identity fields and malformed digests', () => {
const base = {
controllerId: 'controller-1',
operationId: 'operation-1',
scope: {
kind: 'workspace' as const,
workspaceIdentity: 'workspace-1'
},
method: 'workspace/writeTextAtomic',
payloadDigest: `sha256:${'a'.repeat(64)}`
}
expect(operationIdentitySchema.parse(base)).toEqual(base)
expect(() =>
operationIdentitySchema.parse({ ...base, transportGeneration: 4 })
).toThrow()
expect(() =>
operationIdentitySchema.parse({
...base,
payloadDigest: 'not-a-digest'
})
).toThrow()
})
it('canonicalizes object keys without changing array order', () => {
expect(
canonicalJson({
z: 1,
nested: { beta: true, alpha: null },
list: [3, 2, 1]
})
).toBe(
'{"list":[3,2,1],"nested":{"alpha":null,"beta":true},"z":1}'
)
expect(canonicalJson({ value: -0 })).toBe('{"value":0}')
})
it('produces the same operation digest for equivalent key order', async () => {
const scope = {
kind: 'workspace' as const,
workspaceIdentity: 'workspace-1'
}
const first = await digestCanonicalOperation({
method: 'workspace/writeTextAtomic',
scope,
payload: { path: 'a.ts', content: 'x' }
})
const second = await digestCanonicalOperation({
method: 'workspace/writeTextAtomic',
scope,
payload: { content: 'x', path: 'a.ts' }
})
expect(first).toBe(second)
expect(first).toMatch(/^sha256:[a-f0-9]{64}$/u)
})
it('rejects non-JSON and cyclic digest inputs instead of silently dropping values', () => {
expect(() => canonicalJson({ missing: undefined })).toThrow(
CanonicalPayloadError
)
expect(() => canonicalJson(Number.POSITIVE_INFINITY)).toThrow(
CanonicalPayloadError
)
const cyclic: { self?: unknown } = {}
cyclic.self = cyclic
expect(() => canonicalJson(cyclic)).toThrow(CanonicalPayloadError)
})
it('strictly distinguishes all JSON-RPC message variants', () => {
const messages = [
{
jsonrpc: '2.0',
id: 1,
method: 'agent/status',
params: {}
},
{
jsonrpc: '2.0',
method: 'channel/close',
params: { channelId: 'channel-1', channelEpoch: '1' }
},
{ jsonrpc: '2.0', id: 'request-1', result: null },
{
jsonrpc: '2.0',
id: null,
error: { code: -32700, message: 'Parse error' }
}
]
for (const message of messages) {
expect(jsonRpcMessageSchema.parse(message)).toEqual(message)
}
expect(() =>
jsonRpcMessageSchema.parse({
...messages[0],
result: null
})
).toThrow()
expect(() =>
jsonRpcMessageSchema.parse({
jsonrpc: '2.0',
id: null,
method: 'agent/status'
})
).toThrow()
expect(() =>
jsonRpcMessageSchema.parse({
...messages[1],
unexpected: true
})
).toThrow()
})
it('validates attach, capability, and controller synchronization boundaries', () => {
const preface = {
type: 'goodbuddy-agent-attach' as const,
protocol: AGENT_PROTOCOL_VERSION,
goodBuddyVersion: '0.11.0',
controllerId: 'controller-1',
clientNonce: 'nonce-1',
hostRevision: 2,
hostKeyGeneration: 3
}
expect(attachPrefaceSchema.parse(preface)).toEqual(preface)
expect(() =>
attachPrefaceSchema.parse({ ...preface, connectionId: 'client-chosen' })
).toThrow()
const capabilities = {
generation: 1,
capabilities: [
{ name: 'workspace/read', version: 1, critical: true }
],
runtimes: []
}
expect(daemonCapabilitiesSchema.parse(capabilities)).toEqual(
capabilities
)
expect(() =>
daemonCapabilitiesSchema.parse({
...capabilities,
capabilities: [
...capabilities.capabilities,
capabilities.capabilities[0]
]
})
).toThrow(/unique/iu)
const resume = {
previousGeneration: 1,
previousConnectionId: 'connection-1',
daemonBootId: 'boot-1',
capabilityGeneration: 1
}
expect(controllerResumeRequestSchema.parse(resume)).toEqual(resume)
expect(() =>
controllerResumeRequestSchema.parse({
...resume,
controllerId: 'controller-from-client'
})
).toThrow()
expect(() =>
controllerResumeRequestSchema.parse({
...resume,
eventCursors: []
})
).toThrow()
const resumeResult = {
resumed: true,
generation: 2,
daemonBootId: 'boot-1',
capabilityGeneration: 1,
leaseDeadlineAt: '2030-01-01T00:00:00.000Z'
}
expect(controllerResumeResultSchema.parse(resumeResult)).toEqual(
resumeResult
)
expect(() =>
controllerResumeResultSchema.parse({
...resumeResult,
synchronizationRequired: true
})
).toThrow()
})
it('accepts only the detached on-demand Agent supervisor', () => {
const status = {
state: 'ready' as const,
installationId: 'agent-v1',
binaryDigest: `sha256:${'a'.repeat(64)}`,
daemonBootId: 'boot-1',
agentVersion: '1.0.0',
protocol: AGENT_PROTOCOL_VERSION,
platform: 'linux' as const,
architecture: 'x64' as const,
supervisor: 'detached-on-demand' as const,
remoteUserIdentity: 'user-1',
draining: false
}
expect(daemonStatusSchema.parse(status)).toEqual(status)
expect(() =>
daemonStatusSchema.parse({
...status,
supervisor: 'systemd-user'
})
).toThrow()
})
it('types the two-phase ACP resume with stable binding identity and authoritative cursors', () => {
expect(
acpResumeChannelResultSchema.parse({
bindingId: 'binding-1',
channelId: 'binding-1',
channelEpoch: '9',
deadlineAt: '2030-01-01T00:00:00.000Z',
cursors: {
lastOutboundJournaledSequence: '3',
lastOutboundDeliveredSequence: '2',
lastInboundJournaledSequence: '5',
lastMainAckSequence: '4'
}
})
).toMatchObject({
bindingId: 'binding-1',
channelEpoch: '9'
})
expect(() =>
acpReplayChannelRequestSchema.parse({
bindingId: 'binding-1',
channelId: 'binding-1',
channelEpoch: '9',
acknowledgedSequence: '4',
providerResponse: { secret: true }
})
).toThrow()
})
})
describe('agent binary frames', () => {
it('sequences data and durable ACK frames independently', () => {
const channel = new AgentChannelState({
connectionId: 'connection-1',
generation: 3,
channelId: 'acp-1',
channelEpoch: '7',
inboundDirection: 'main-to-agent'
})
const inbound = (
kind: AgentFrameHeader['kind'],
sequence: string
): AgentFrameHeader => ({
...header,
channelId: 'acp-1',
direction: 'main-to-agent',
kind,
sequence
})
expect(() => channel.acceptInbound(inbound('acp', '1'))).not.toThrow()
expect(() =>
channel.acceptInbound(inbound('ack', '1'))
).not.toThrow()
expect(() => channel.acceptInbound(inbound('acp', '2'))).not.toThrow()
const outbound = (
kind: AgentFrameHeader['kind']
): AgentFrameHeader =>
channel.reserveOutbound({
...header,
channelId: 'acp-1',
direction: 'agent-to-main',
kind
})
expect(outbound('acp').sequence).toBe('1')
expect(outbound('ack').sequence).toBe('1')
expect(outbound('acp').sequence).toBe('2')
})
it('round-trips raw ACP bytes and all routing identity', () => {
const payload = new Uint8Array([0, 10, 255, 42])
const bytes = encodeAgentFrame({
header: { ...header, kind: 'acp' },
payload
})
const decoded = decodeAgentFrame(bytes, {
protocolMajor: AGENT_PROTOCOL_VERSION.major,
maximumProtocolMinor: AGENT_PROTOCOL_VERSION.minor,
connectionId: 'connection-1',
generation: 3
})
expect(decoded.header).toEqual({
...header,
kind: 'acp',
payloadLength: payload.byteLength
})
expect(decoded.payload).toEqual(payload)
})
it.each([
['control', AGENT_PROTOCOL_LIMITS.maximumControlFrameBytes],
['acp', AGENT_PROTOCOL_LIMITS.maximumAcpFrameBytes],
['blob', AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes],
['ack', AGENT_PROTOCOL_LIMITS.maximumAckFrameBytes]
] as const)('enforces the %s frame limit', (kind, maximum) => {
expect(() =>
agentFrameHeaderSchema.parse({
...header,
kind,
payloadLength: maximum + 1
})
).toThrow()
})
it('keeps the encoded frame bound aligned with maximum identifiers and payload', () => {
const maximumIdentifier = 'a'.repeat(
AGENT_PROTOCOL_LIMITS.maximumIdentifierBytes
)
const encoded = encodeAgentFrame({
header: {
...header,
connectionId: maximumIdentifier,
channelId: maximumIdentifier,
kind: 'blob'
},
payload: new Uint8Array(
AGENT_PROTOCOL_LIMITS.maximumBlobFrameBytes
)
})
expect(encoded.byteLength).toBe(MAXIMUM_ENCODED_AGENT_FRAME_BYTES)
})
it('rejects generation mismatch, trailing bytes, and incomplete frames', () => {
const encoded = encodeAgentFrame({
header,
payload: new Uint8Array([1])
})
expect(() =>
decodeAgentFrame(encoded, { generation: 4 })
).toThrow(/generation/iu)
const trailing = new Uint8Array(encoded.byteLength + 1)
trailing.set(encoded)
expect(() => decodeAgentFrame(trailing)).toThrow(/Trailing/iu)
expect(() =>
decodeAgentFrame(encoded.subarray(0, encoded.byteLength - 1))
).toThrow(/Incomplete/iu)
})
it('rejects unnegotiated major and minor versions', () => {
const encoded = encodeAgentFrame({
header: { ...header, protocolMajor: 2, protocolMinor: 4 },
payload: new Uint8Array()
})
expect(() =>
decodeAgentFrame(encoded, {
protocolMajor: 1,
maximumProtocolMinor: 3
})
).toThrow(/major/iu)
expect(() =>
decodeAgentFrame(encoded, {
protocolMajor: 2,
maximumProtocolMinor: 3
})
).toThrow(/minor/iu)
})
})