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
@@ -0,0 +1,174 @@
import { describe, expect, it } from 'vitest'
import {
agentBundleManifestSchema,
agentReleaseKeyRegistrySchema,
agentRuntimeLockSchema
} from './agent-installation-contracts'
const digest = 'a'.repeat(64)
function manifest() {
return {
formatVersion: 1,
product: 'GoodBuddy',
agentVersion: '0.11.0',
platform: 'linux',
arch: 'x64',
protocol: { major: 1, minor: 0 },
signingKeyId: 'test-key',
entrypoint: {
path: 'goodbuddy-agent',
runtimePath: 'node',
scriptPath: 'lib/agent.cjs'
},
files: [
{
path: 'goodbuddy-agent',
size: 1,
sha256: digest,
mode: '0755'
},
{
path: 'node',
size: 1,
sha256: digest,
mode: '0755'
},
{
path: 'lib/agent.cjs',
size: 1,
sha256: digest,
mode: '0644'
},
{
path: 'licenses/license.txt',
size: 1,
sha256: digest,
mode: '0644'
}
],
licenses: [
{
package: 'fixture',
version: '1.0.0',
spdx: 'MIT',
path: 'licenses/license.txt'
}
]
} as const
}
describe('Agent installation contracts', () => {
it('accepts a complete strict manifest', () => {
expect(agentBundleManifestSchema.parse(manifest())).toMatchObject({
arch: 'x64'
})
expect(() =>
agentBundleManifestSchema.parse({
...manifest(),
agentVersion: 'release_10'
})
).toThrow()
})
it('rejects unsafe, duplicate, undeclared, and incorrectly-modeled files', () => {
expect(() =>
agentBundleManifestSchema.parse({
...manifest(),
files: [
...manifest().files,
{
path: '../outside',
size: 1,
sha256: digest,
mode: '0644'
}
]
})
).toThrow()
expect(() =>
agentBundleManifestSchema.parse({
...manifest(),
files: [...manifest().files, manifest().files[0]]
})
).toThrow()
expect(() =>
agentBundleManifestSchema.parse({
...manifest(),
licenses: [
{
package: 'fixture',
version: '1.0.0',
spdx: 'MIT',
path: 'licenses/missing.txt'
}
]
})
).toThrow()
expect(() =>
agentBundleManifestSchema.parse({
...manifest(),
files: manifest().files.map((file) =>
file.path === 'goodbuddy-agent'
? { ...file, mode: '0644' }
: file
)
})
).toThrow()
})
it('rejects duplicate keys and extra registry fields', () => {
const key = {
keyId: 'test-key',
publicKeySpkiBase64: 'AAAA',
environment: 'test'
}
expect(() =>
agentReleaseKeyRegistrySchema.parse({
formatVersion: 1,
keys: [key, key],
revocations: [],
unknown: true
})
).toThrow()
})
it('requires both locked Linux targets and the fixed Node patch', () => {
const target = {
archive: 'node.tar.gz',
sha256: digest,
binaryPath: 'node/bin/node',
licensePath: 'node/LICENSE'
}
expect(
agentRuntimeLockSchema.parse({
formatVersion: 1,
agentVersion: '0.11.0',
protocol: { major: 1, minor: 0 },
node: {
version: '24.19.0',
source: 'https://nodejs.org/dist/v24.19.0/',
targets: {
'linux-x64': target,
'linux-arm64': target
}
},
koffi: { version: '3.1.4' }
}).koffi.version
).toBe('3.1.4')
expect(() =>
agentRuntimeLockSchema.parse({
formatVersion: 1,
agentVersion: '0.11.0',
protocol: { major: 1, minor: 0 },
node: {
version: '24.18.0',
source: 'http://nodejs.org/',
targets: { 'linux-x64': target }
},
koffi: { version: '3.1.3' }
})
).toThrow()
})
})
+290
View File
@@ -0,0 +1,290 @@
import { z } from 'zod'
const versionPattern =
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+((?:[0-9a-zA-Z-]+)(?:\.[0-9a-zA-Z-]+)*))?$/u
const sha256Pattern = /^[a-f0-9]{64}$/u
const keyIdPattern = /^[a-z0-9][a-z0-9._-]{0,63}$/u
const canonicalBase64Pattern =
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u
export const agentArchitectureSchema = z.enum(['x64', 'arm64'])
export const componentVersionSchema = z
.string()
.max(128)
.regex(versionPattern)
export type AgentArchitecture = z.infer<
typeof agentArchitectureSchema
>
export const agentProtocolVersionSchema = z
.object({
major: z.number().int().min(0).max(65_535).safe(),
minor: z.number().int().min(0).max(65_535).safe()
})
.strict()
export const agentManifestPathSchema = z
.string()
.min(1)
.max(240)
.refine(isSafeAgentManifestPath, 'Unsafe Agent manifest path')
export const agentManifestFileSchema = z
.object({
path: agentManifestPathSchema,
size: z.number().int().min(0).safe(),
sha256: z.string().regex(sha256Pattern),
mode: z.enum(['0644', '0755'])
})
.strict()
.superRefine((file, context) => {
const expectedMode = expectedAgentManifestMode(file.path)
if (file.mode !== expectedMode) {
context.addIssue({
code: 'custom',
path: ['mode'],
message: `Agent payload mode must be ${expectedMode}`
})
}
})
export type AgentManifestFile = z.infer<
typeof agentManifestFileSchema
>
export const agentManifestLicenseSchema = z
.object({
package: z.string().trim().min(1).max(200),
version: z.string().trim().min(1).max(128),
spdx: z.string().trim().min(1).max(128),
path: agentManifestPathSchema
})
.strict()
export const agentBundleManifestSchema = z
.object({
formatVersion: z.literal(1),
product: z.literal('GoodBuddy'),
agentVersion: componentVersionSchema,
platform: z.literal('linux'),
arch: agentArchitectureSchema,
protocol: agentProtocolVersionSchema,
signingKeyId: z.string().regex(keyIdPattern),
entrypoint: z
.object({
path: z.literal('goodbuddy-agent'),
runtimePath: z.literal('node'),
scriptPath: z.literal('lib/agent.cjs')
})
.strict(),
files: z.array(agentManifestFileSchema).min(1).max(10_000),
licenses: z.array(agentManifestLicenseSchema).min(1).max(1_000)
})
.strict()
.superRefine((manifest, context) => {
const declaredPaths = new Set<string>()
manifest.files.forEach((file, index) => {
if (
file.path === 'manifest.json' ||
file.path === 'manifest.sig'
) {
context.addIssue({
code: 'custom',
path: ['files', index, 'path'],
message: 'Agent payload cannot declare signature metadata'
})
}
if (declaredPaths.has(file.path)) {
context.addIssue({
code: 'custom',
path: ['files', index, 'path'],
message: 'Agent manifest file paths must be unique'
})
}
declaredPaths.add(file.path)
})
const licensedPaths = new Set<string>()
manifest.licenses.forEach((license, index) => {
if (licensedPaths.has(license.path)) {
context.addIssue({
code: 'custom',
path: ['licenses', index, 'path'],
message: 'Agent manifest license paths must be unique'
})
}
if (!declaredPaths.has(license.path)) {
context.addIssue({
code: 'custom',
path: ['licenses', index, 'path'],
message: 'Agent manifest license must name a declared file'
})
}
licensedPaths.add(license.path)
})
for (const path of [
manifest.entrypoint.path,
manifest.entrypoint.runtimePath,
manifest.entrypoint.scriptPath
]) {
if (!declaredPaths.has(path)) {
context.addIssue({
code: 'custom',
path: ['entrypoint'],
message: `Agent entrypoint payload is missing: ${path}`
})
}
}
})
export type AgentBundleManifest = z.infer<
typeof agentBundleManifestSchema
>
export const agentReleaseKeySchema = z
.object({
keyId: z.string().regex(keyIdPattern),
publicKeySpkiBase64: z
.string()
.min(4)
.max(4_096)
.regex(canonicalBase64Pattern),
environment: z.enum(['production', 'test'])
})
.strict()
export type AgentReleaseKey = z.infer<typeof agentReleaseKeySchema>
export const agentReleaseKeyRevocationSchema = z
.object({
keyId: z.string().regex(keyIdPattern)
})
.strict()
export const agentReleaseKeyRegistrySchema = z
.object({
formatVersion: z.literal(1),
keys: z.array(agentReleaseKeySchema).max(1_000),
revocations: z.array(agentReleaseKeyRevocationSchema).max(1_000)
})
.strict()
.superRefine((registry, context) => {
addDuplicateKeyIssues(
registry.keys,
'keys',
context
)
addDuplicateKeyIssues(
registry.revocations,
'revocations',
context
)
})
export type AgentReleaseKeyRegistry = z.infer<
typeof agentReleaseKeyRegistrySchema
>
const lockedRuntimePathSchema = z
.string()
.min(1)
.max(512)
.refine(isSafeAgentManifestPath, 'Unsafe locked runtime path')
const lockedNodeTargetSchema = z
.object({
archive: z
.string()
.min(1)
.max(240)
.refine(
(value) =>
!value.includes('/') &&
!value.includes('\\') &&
value !== '.' &&
value !== '..',
'Locked Node archive name is invalid'
),
sha256: z.string().regex(sha256Pattern),
binaryPath: lockedRuntimePathSchema,
licensePath: lockedRuntimePathSchema
})
.strict()
export const agentRuntimeLockSchema = z
.object({
formatVersion: z.literal(1),
agentVersion: componentVersionSchema,
protocol: agentProtocolVersionSchema,
node: z
.object({
version: z.literal('24.19.0'),
source: z
.url()
.refine(
(value) => new URL(value).protocol === 'https:',
'Locked Node source must use HTTPS'
),
targets: z
.object({
'linux-x64': lockedNodeTargetSchema,
'linux-arm64': lockedNodeTargetSchema
})
.strict()
})
.strict(),
koffi: z
.object({
version: z.literal('3.1.4')
})
.strict()
})
.strict()
export type AgentRuntimeLock = z.infer<typeof agentRuntimeLockSchema>
export function isSafeAgentManifestPath(filePath: string): boolean {
return (
filePath.length > 0 &&
filePath.length <= 512 &&
!filePath.includes('\\') &&
!filePath.includes('\0') &&
!filePath.startsWith('/') &&
!filePath
.split('/')
.some(
(part) => part.length === 0 || part === '.' || part === '..'
)
)
}
export function expectedAgentManifestMode(
filePath: string
): '0644' | '0755' {
return filePath === 'node' ||
filePath === 'goodbuddy-agent' ||
filePath.startsWith('helpers/')
? '0755'
: '0644'
}
function addDuplicateKeyIssues(
entries: readonly { keyId: string }[],
path: 'keys' | 'revocations',
context: z.RefinementCtx
): void {
const ids = new Set<string>()
entries.forEach((entry, index) => {
if (ids.has(entry.keyId)) {
context.addIssue({
code: 'custom',
path: [path, index, 'keyId'],
message: `Agent ${path} key IDs must be unique`
})
}
ids.add(entry.keyId)
})
}
+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)
})
})
+190
View File
@@ -6,7 +6,11 @@ import {
conversationBranchInputSchema,
conversationSnapshotSchema,
isUntouchedBuiltInDefaultProject,
persistedProjectExecutionSpaceSchema,
projectCreateSchema,
projectExecutionSpaceSchema,
projectRuntimeValidationSchema,
sshExecutionValidationSchema,
projectUpdateSchema
} from './assistant-contracts'
@@ -15,6 +19,10 @@ const untouchedProject: AssistantProject = {
name: builtInDefaultProjectSeedName,
description: builtInDefaultProjectSeedDescription,
rootPath: 'C:\\Workspace',
executionSpace: {
kind: 'local',
rootPath: 'C:\\Workspace'
},
defaultWorkMode: 'ask',
kind: 'user',
builtInDefault: true,
@@ -75,6 +83,188 @@ describe('isUntouchedBuiltInDefaultProject', () => {
})
})
describe('project execution space contracts', () => {
it('parses strict local and SSH output without normalizing paths', () => {
expect(
projectExecutionSpaceSchema.parse({
kind: 'local',
rootPath: ''
})
).toEqual({ kind: 'local', rootPath: '' })
expect(
projectExecutionSpaceSchema.parse({
kind: 'local',
rootPath: ' '
})
).toEqual({ kind: 'local', rootPath: ' ' })
expect(
projectExecutionSpaceSchema.parse({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy'
})
).toEqual({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy'
})
expect(
projectExecutionSpaceSchema.safeParse({
kind: 'local',
rootPath: 'C:\\Workspace',
hostId: '00000000-0000-4000-8000-000000000301'
}).success
).toBe(false)
expect(
projectExecutionSpaceSchema.safeParse({
kind: 'ssh',
hostId: 'not-a-host-id',
remoteRootPath: '/srv/goodbuddy'
}).success
).toBe(false)
expect(
projectExecutionSpaceSchema.safeParse({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: ' '
}).success
).toBe(false)
})
it('keeps SSH and Runtime validation identities separate and strict', () => {
const validatedAt = '2026-08-21T00:00:00.000Z'
const validation = {
hostRevision: 2,
hostKeyGeneration: 3,
remoteUsername: 'builder',
workspaceIdentity: 'workspace-identity',
agentProtocolMajor: 1,
agentInstallationIdAtValidation: 'agent-installation',
agentBinaryDigestAtValidation: `sha256:${'a'.repeat(64)}`,
agentVersionAtValidation: '0.11.0',
agentArchitectureAtValidation: 'x64',
validatedAt
}
const runtimeValidation = {
runtimeSelectionKey: 'opencode:profile',
runtimeBundleDigest: `sha256:${'b'.repeat(64)}`,
runtimeAdapterDigest: `sha256:${'c'.repeat(64)}`,
agentInstallationIdAtValidation: 'agent-installation',
validatedAt,
workMode: 'execute'
}
expect(
sshExecutionValidationSchema.safeParse(validation).success
).toBe(true)
expect(
projectRuntimeValidationSchema.safeParse(runtimeValidation)
.success
).toBe(true)
expect(
persistedProjectExecutionSpaceSchema.parse({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy',
validation
})
).toEqual({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy',
validation
})
expect(
sshExecutionValidationSchema.safeParse({
hostRevision: 2,
hostKeyGeneration: 3,
remoteUsername: 'builder',
workspaceIdentity: 'workspace-identity',
agentProtocolMajor: 1,
agentInstallationIdAtValidation: 'agent-installation',
agentBinaryDigestAtValidation: `sha256:${'a'.repeat(64)}`,
agentVersionAtValidation: '0.11.0',
agentArchitectureAtValidation: 'x64',
validatedAt,
runtimeBundleDigest: 'must-not-be-here'
}).success
).toBe(false)
expect(
persistedProjectExecutionSpaceSchema.safeParse({
kind: 'local',
rootPath: 'C:\\Workspace',
validation
}).success
).toBe(false)
expect(
persistedProjectExecutionSpaceSchema.safeParse({
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy',
hostRevision: 2
}).success
).toBe(false)
for (const invalid of [
{ ...validation, hostRevision: 0 },
{ ...validation, trustAttestationRevision: 4 },
{
...validation,
agentBinaryDigestAtValidation: `sha256:${'A'.repeat(64)}`
},
{ ...validation, agentVersionAtValidation: ' version ' },
{ ...validation, agentArchitectureAtValidation: 'ia32' }
]) {
expect(
sshExecutionValidationSchema.safeParse(invalid).success
).toBe(false)
}
for (const invalid of [
{
...runtimeValidation,
runtimeBundleDigest: 'runtime-digest'
},
{ ...runtimeValidation, trustTier: 'T3' },
{
...runtimeValidation,
confinementPolicyDigest: `sha256:${'d'.repeat(64)}`
},
{ ...runtimeValidation, approvalBridgeVersion: '1' },
{
...runtimeValidation,
workMode: 'plan'
}
]) {
expect(
projectRuntimeValidationSchema.safeParse(invalid).success
).toBe(false)
}
expect(
projectRuntimeValidationSchema.safeParse({
...runtimeValidation,
workMode: 'ask'
}).success
).toBe(true)
})
it('keeps renderer project create and update inputs local-only', () => {
const input = {
name: '远程草稿',
description: '',
rootPath: '/srv/goodbuddy',
defaultWorkMode: 'ask',
executionSpace: {
kind: 'ssh',
hostId: '00000000-0000-4000-8000-000000000301',
remoteRootPath: '/srv/goodbuddy'
}
}
expect(projectCreateSchema.safeParse(input).success).toBe(false)
expect(projectUpdateSchema.safeParse(input).success).toBe(false)
})
})
describe('conversation branch contracts', () => {
it('accepts bounded branch provenance and rejects renderer extras', () => {
const sourceConversationId =
+85
View File
@@ -1,5 +1,8 @@
import { z } from 'zod'
import { agentArchitectureSchema } from './agent-installation-contracts'
import { sha256DigestSchema } from './agent-protocol/contracts'
import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
import { sshHostIdSchema } from './ssh-host-contracts'
export const assistantIdSchema = z.string().uuid()
export const interactiveWorkModes = ['ask', 'execute'] as const
@@ -28,6 +31,86 @@ export type InteractiveWorkMode = (typeof interactiveWorkModes)[number]
export type ProjectKind = z.infer<typeof projectKindSchema>
export type ProjectChannel = z.infer<typeof projectChannelSchema>
const projectExecutionSpacePathSchema = z.string().max(4_096)
const projectExecutionValidationTextSchema = z
.string()
.min(1)
.max(1_000)
.refine((value) => value.trim().length > 0)
export const projectExecutionSpaceSchema = z.discriminatedUnion(
'kind',
[
z
.object({
kind: z.literal('local'),
rootPath: projectExecutionSpacePathSchema
})
.strict(),
z
.object({
kind: z.literal('ssh'),
hostId: sshHostIdSchema,
remoteRootPath: projectExecutionSpacePathSchema.refine(
(value) => value.trim().length > 0,
'远程项目目录不能为空'
)
})
.strict()
]
)
export const sshExecutionValidationSchema = z
.object({
hostRevision: z.number().int().positive(),
hostKeyGeneration: z.number().int().positive(),
remoteUsername: projectExecutionValidationTextSchema,
workspaceIdentity: projectExecutionValidationTextSchema,
agentProtocolMajor: z.number().int().positive(),
agentInstallationIdAtValidation:
projectExecutionValidationTextSchema,
agentBinaryDigestAtValidation: sha256DigestSchema,
agentVersionAtValidation: z
.string()
.regex(/^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/u),
agentArchitectureAtValidation: agentArchitectureSchema,
validatedAt: z.string().datetime({ offset: true })
})
.strict()
export const projectRuntimeValidationSchema = z
.object({
runtimeSelectionKey: z.string().trim().min(1).max(1_000),
runtimeBundleDigest: sha256DigestSchema,
runtimeAdapterDigest: sha256DigestSchema,
agentInstallationIdAtValidation:
projectExecutionValidationTextSchema,
validatedAt: z.string().datetime({ offset: true }),
workMode: workModeSchema
})
.strict()
export type SshExecutionValidation = z.output<
typeof sshExecutionValidationSchema
>
export type ProjectRuntimeValidation = z.output<
typeof projectRuntimeValidationSchema
>
export const persistedProjectExecutionSpaceSchema =
z.discriminatedUnion('kind', [
projectExecutionSpaceSchema.options[0],
projectExecutionSpaceSchema.options[1]
.extend({
validation: sshExecutionValidationSchema.optional()
})
.strict()
])
export type ProjectExecutionSpace = z.output<
typeof persistedProjectExecutionSpaceSchema
>
export function normalizeInteractiveWorkMode(
workMode: LegacyWorkMode | undefined
): InteractiveWorkMode {
@@ -429,6 +512,8 @@ export type LocalConversationSaveBatch = z.infer<
export type AssistantProject = ProjectCreateInput & {
id: string
kind: ProjectKind
executionSpace: ProjectExecutionSpace
runtimeValidation?: ProjectRuntimeValidation
channel?: ProjectChannel
readonly builtInDefault?: boolean
status: 'active' | 'archived'
+99
View File
@@ -0,0 +1,99 @@
import { describe, expect, it } from 'vitest'
import {
agentRequestSchema,
builtinEmbeddingConnectionId,
legacyEmbeddingConnectionId,
runtimeSettingsInputSchema
} from './contracts'
const baseInput = {
provider: 'model' as const,
modelBaseUrl: 'https://bigtoken.ai',
modelName: 'sonnet-5',
modelProtocol: 'anthropic-messages' as const,
modelAuthentication: 'api-key' as const,
imageGenerationQuality: 'auto' as const,
opencodeBaseUrl: '',
opencodeEmbedded: true,
opencodeBinaryPath: '',
opencodeConfigPath: '',
continueBinaryPath: '',
continueConfigPath: '',
continueMode: 'chat' as const,
knowledgeEmbeddingEnabled: true,
knowledgeEmbeddingBaseUrl: 'https://vectors.example/v1/embeddings',
knowledgeEmbeddingModel: 'embed-large',
knowledgeRerankEnabled: false,
knowledgeRerankEndpoint: 'https://rerank.example/v1/rerank',
knowledgeRerankModel: 'rerank-v3.5',
workspacePath: 'workspace',
apiKey: { action: 'keep' as const },
deepseekHarnessModelSource: { kind: 'platform' as const },
toolApproval: 'always' as const
}
describe('embedding connection settings input', () => {
it('accepts additive builtin and user connection updates', () => {
expect(
runtimeSettingsInputSchema.safeParse({
...baseInput,
embeddingConnections: [
{
id: builtinEmbeddingConnectionId,
name: 'GoodBuddy 内置向量模型',
kind: 'builtin'
},
{
id: legacyEmbeddingConnectionId,
name: '自定义向量模型',
kind: 'openai-compatible',
baseUrl: 'https://vectors.example/v1/embeddings',
modelName: 'embed-large',
authentication: 'api-key',
apiKey: { action: 'keep' }
}
],
activeEmbeddingConnectionId: legacyEmbeddingConnectionId
}).success
).toBe(true)
})
it('rejects missing builtin, dangling active IDs, and keys on no-auth connections', () => {
const connection = {
id: legacyEmbeddingConnectionId,
name: '自定义向量模型',
kind: 'openai-compatible' as const,
baseUrl: 'https://vectors.example/v1/embeddings',
modelName: 'embed-large',
authentication: 'none' as const,
apiKey: { action: 'replace' as const, value: 'secret' }
}
expect(
runtimeSettingsInputSchema.safeParse({
...baseInput,
embeddingConnections: [connection],
activeEmbeddingConnectionId: crypto.randomUUID()
}).success
).toBe(false)
})
it('keeps embedding configuration and vectors outside Agent Runtime requests', () => {
const request = {
requestId: crypto.randomUUID(),
conversationId: 'conversation',
prompt: 'hello',
knowledgeLibraryIds: [],
knowledgeRetrievalMode: 'auto' as const
}
expect(agentRequestSchema.safeParse(request).success).toBe(true)
for (const forbidden of [
{ embeddingApiKey: 'secret' },
{ embeddingConfiguration: { endpoint: 'https://vectors.example' } },
{ embeddingVectors: [[0.1, 0.2]] }
]) {
expect(
agentRequestSchema.safeParse({ ...request, ...forbidden }).success
).toBe(false)
}
})
})
+49
View File
@@ -136,6 +136,10 @@ import type { KnowledgeOntologySettings } from './knowledge-ontology'
import type {
KnowledgeTaskItem
} from './knowledge-task-contracts'
import type {
RemoteProjectSaveProgress,
RemoteProjectSaveRequest
} from './remote-project-candidate-contracts'
export type {
KnowledgeTaskError,
KnowledgeTaskItem,
@@ -144,8 +148,24 @@ export type {
KnowledgeTaskStage,
KnowledgeTaskStatus
} from './knowledge-task-contracts'
export type {
RemoteProjectCreateDraft,
RemoteProjectSavePhase,
RemoteProjectSaveProgress,
RemoteProjectSaveRequest,
RemoteProjectUpdateDraft
} from './remote-project-candidate-contracts'
import type { WeixinBindingSnapshot } from './weixin-channel-contracts'
import type { RemoteChannelActivity } from './remote-channel-contracts'
import type {
SshHostDraftInspectionRequest,
SshHostKeyInspection,
SshHostRemoteEnvironment,
SshHostsSnapshot,
SshDirectoryBrowseResult,
SshHostValidationRequest,
SshHostValidationResult
} from './ssh-host-contracts'
import {
agentRuntimeSelectionSchema,
type AgentRuntimeSelection
@@ -1522,6 +1542,25 @@ export type DesktopApi = {
selection: AgentRuntimeSelection
) => Promise<AgentRuntimeStatus>
}
sshHosts?: {
getSnapshot: () => Promise<SshHostsSnapshot>
remove: (hostId: string) => Promise<void>
inspectDraftHostKey: (
input: SshHostDraftInspectionRequest
) => Promise<SshHostKeyInspection>
discardCandidate: (candidateId: string) => Promise<void>
validateAndSave: (
input: SshHostValidationRequest
) => Promise<SshHostValidationResult>
getRemoteEnvironment: (
hostId: string
) => Promise<SshHostRemoteEnvironment>
browseDirectories: (
hostId: string,
path?: string
) => Promise<SshDirectoryBrowseResult>
cancelDirectoryBrowse: () => Promise<void>
}
channels?: {
getSnapshot: () => Promise<ChannelSettingsSnapshot>
apply: (input: ChannelSettingsApply) => Promise<ChannelSettingsSnapshot>
@@ -1648,6 +1687,16 @@ export type DesktopApi = {
) => Promise<AssistantProject>
setArchived: (projectId: string, archived: boolean) => Promise<void>
delete: (projectId: string, confirmation: string) => Promise<void>
remote: {
activate: (projectId: string) => Promise<AssistantProject>
save: (
input: RemoteProjectSaveRequest
) => Promise<AssistantProject>
cancelCurrent: () => Promise<void>
onSaveProgress: (
listener: (progress: RemoteProjectSaveProgress) => void
) => () => void
}
}
conversations: {
list: () => Promise<ConversationSnapshot[]>
+17
View File
@@ -34,6 +34,19 @@ export const ipcChannels = {
runtimeCustomizationGet: 'settings:runtime-customization:get',
runtimeCustomizationUpdate: 'settings:runtime-customization:update',
runtimeNativeSnapshot: 'settings:runtime-native:snapshot',
sshHostsGet: 'settings:ssh-hosts:get',
sshHostsRemove: 'settings:ssh-hosts:remove',
sshHostsInspectDraftKey: 'settings:ssh-hosts:inspect-draft-key',
sshHostsDiscardCandidate:
'settings:ssh-hosts:discard-candidate',
sshHostsValidateAndSave:
'settings:ssh-hosts:validate-and-save',
sshHostsRemoteEnvironment:
'settings:ssh-hosts:remote-environment',
sshHostsBrowseDirectories:
'settings:ssh-hosts:directories:browse',
sshHostsCancelDirectoryBrowse:
'settings:ssh-hosts:directories:cancel',
channelSettingsGet: 'settings:channels:get',
channelSettingsApply: 'settings:channels:apply',
channelSettingsTest: 'settings:channels:test',
@@ -98,6 +111,10 @@ export const ipcChannels = {
projectsUpdate: 'projects:update',
projectsSetArchived: 'projects:set-archived',
projectsDelete: 'projects:delete',
remoteProjectActivate: 'projects:remote:activate',
remoteProjectSave: 'projects:remote:save',
remoteProjectCancelCurrent: 'projects:remote:cancel-current',
remoteProjectSaveProgress: 'projects:remote:save-progress',
conversationsList: 'conversations:list',
conversationsReplace: 'conversations:replace',
conversationsSaveLocal: 'conversations:save-local',
+229
View File
@@ -0,0 +1,229 @@
import { describe, expect, it } from 'vitest'
import {
MODEL_BRIDGE_LIMITS,
MODEL_BRIDGE_PROTOCOL,
ModelBridgeCodecError,
createModelBridgeRequestMessage,
decodeModelBridgeMessage,
digestModelBridgeRequest,
encodeModelBridgeMessage,
modelBridgeErrorMessageSchema,
modelBridgeDeliveryAckMessageSchema,
modelBridgeIdentitySchema,
modelBridgePolicySchema,
type ModelBridgeIdentity
} from './model-bridge-contracts'
import { remoteModelGatewayRequestSchema } from './remote-model-gateway-contracts'
const digest = `sha256:${'a'.repeat(64)}`
const identity: ModelBridgeIdentity = {
bindingId: 'binding-1',
promptOperationId: 'prompt-1',
requestId: 'request-1',
roundIndex: 2,
modelProfileDigest: digest,
messageId: 'message-1'
}
const policy = {
protocol: 'anthropic-messages',
model: 'claude-test',
modelProfileDigest: digest,
supportsImageInput: true
} as const
const request = {
method: 'POST',
path: '/v1/messages',
headers: {
accept: 'application/json',
'content-type': 'application/json'
},
bodyBase64: Buffer.from(
JSON.stringify({
model: 'claude-test',
messages: [{ role: 'user', content: '你好' }]
})
).toString('base64')
} as const
describe('model bridge contracts and codec', () => {
it('round trips one bounded request message with an exact digest', async () => {
const message = await createModelBridgeRequestMessage({
identity,
policy,
request
})
expect(message.requestDigest).toBe(
await digestModelBridgeRequest(request)
)
const payload = await encodeModelBridgeMessage(message)
expect(payload.byteLength).toBeLessThanOrEqual(
MODEL_BRIDGE_LIMITS.maximumMessageBytes
)
await expect(
decodeModelBridgeMessage(payload, { expectedIdentity: identity })
).resolves.toEqual(message)
})
it('uses representation safety rather than a fixed round quota', () => {
expect(
modelBridgeIdentitySchema.parse({
...identity,
roundIndex: 100_001
}).roundIndex
).toBe(100_001)
})
it('deterministically encodes a large request as one message', async () => {
const message = await createModelBridgeRequestMessage({
identity,
policy,
request: {
...request,
bodyBase64: Buffer.alloc(600 * 1024, 7).toString('base64')
}
})
const left = await encodeModelBridgeMessage(message)
const right = await encodeModelBridgeMessage(message)
expect(left).toEqual(right)
expect(left.byteLength).toBeGreaterThan(600 * 1024)
await expect(decodeModelBridgeMessage(left)).resolves.toEqual(message)
})
it('round trips response, error, and delivery ACK messages', async () => {
const requestDigest = await digestModelBridgeRequest(request)
const response = {
protocol: MODEL_BRIDGE_PROTOCOL,
kind: 'response',
identity,
requestDigest,
response: {
status: 200,
headers: { 'content-type': 'application/json' },
bodyBase64: Buffer.from('{"ok":true}').toString('base64')
}
} as const
await expect(
decodeModelBridgeMessage(await encodeModelBridgeMessage(response))
).resolves.toEqual(response)
const error = modelBridgeErrorMessageSchema.parse({
protocol: MODEL_BRIDGE_PROTOCOL,
kind: 'error',
identity,
requestDigest,
error: {
code: 'dispatch-outcome-unknown',
message: 'Provider dispatch outcome cannot be established',
retryable: false,
poisoned: true,
outcomeUnknown: true
}
})
await expect(
decodeModelBridgeMessage(await encodeModelBridgeMessage(error))
).resolves.toEqual(error)
const delivery = modelBridgeDeliveryAckMessageSchema.parse({
protocol: MODEL_BRIDGE_PROTOCOL,
kind: 'response-delivered',
identity,
requestDigest
})
await expect(
decodeModelBridgeMessage(await encodeModelBridgeMessage(delivery))
).resolves.toEqual(delivery)
})
it('rejects malformed, noncanonical, and oversized messages', async () => {
await expect(
decodeModelBridgeMessage(new Uint8Array([0xff]))
).rejects.toBeInstanceOf(ModelBridgeCodecError)
await expect(
decodeModelBridgeMessage(Buffer.from('{ "kind": "request" }'))
).rejects.toMatchObject({ code: 'invalid-message' })
const message = await createModelBridgeRequestMessage({
identity,
policy,
request
})
const payload = await encodeModelBridgeMessage(message)
await expect(
decodeModelBridgeMessage(payload, { maximumMessageBytes: 10 })
).rejects.toMatchObject({ code: 'oversized' })
await expect(
decodeModelBridgeMessage(
Buffer.alloc(MODEL_BRIDGE_LIMITS.maximumMessageBytes + 1)
)
).rejects.toMatchObject({ code: 'oversized' })
})
it('rejects mismatched identities and request digests', async () => {
const message = await createModelBridgeRequestMessage({
identity,
policy,
request
})
const payload = await encodeModelBridgeMessage(message)
await expect(
decodeModelBridgeMessage(payload, {
expectedIdentity: { ...identity, roundIndex: 3 }
})
).rejects.toMatchObject({ code: 'identity-mismatch' })
await expect(
decodeModelBridgeMessage(payload, {
expectedRequestDigest: `sha256:${'b'.repeat(64)}`
})
).rejects.toMatchObject({ code: 'digest-mismatch' })
await expect(
encodeModelBridgeMessage({
...message,
requestDigest: `sha256:${'b'.repeat(64)}`
})
).rejects.toMatchObject({ code: 'digest-mismatch' })
expect(() =>
modelBridgePolicySchema.parse({
...policy,
baseUrl: 'https://provider.example',
apiKey: 'must-not-cross'
})
).toThrow()
expect(
modelBridgePolicySchema.parse({
...policy,
maximumOutputTokens: 65_537,
maximumModelCalls: 8,
maximumTotalOutputTokens: 32_768
})
).toMatchObject(policy)
await expect(
createModelBridgeRequestMessage({
identity,
policy: {
...policy,
modelProfileDigest: `sha256:${'b'.repeat(64)}`
},
request
})
).rejects.toThrow()
})
it('rejects noncanonical base64 and protocol path mismatches', async () => {
expect(() =>
remoteModelGatewayRequestSchema.parse({
...request,
bodyBase64: 'AB=='
})
).toThrow()
await expect(
createModelBridgeRequestMessage({
identity,
policy,
request: { ...request, path: '/v1/responses' }
})
).rejects.toThrow()
})
})
+403
View File
@@ -0,0 +1,403 @@
import { z } from 'zod'
import {
agentIdentifierSchema,
sha256DigestSchema,
utf8StringSchema
} from './agent-protocol/contracts'
import { canonicalJson } from './agent-protocol/canonical'
import {
REMOTE_MODEL_GATEWAY_LIMITS,
remoteModelGatewayRequestSchema,
remoteModelGatewayResponseSchema
} from './remote-model-gateway-contracts'
const textEncoder = new TextEncoder()
const fatalTextDecoder = new TextDecoder('utf-8', { fatal: true })
export const MODEL_BRIDGE_LIMITS = {
maximumMessageBytes:
Math.ceil(
Math.max(
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes,
REMOTE_MODEL_GATEWAY_LIMITS.maximumResponseBodyBytes
) / 3
) *
4 +
16 * 1024,
maximumErrorMessageBytes: 2_048,
maximumModelNameBytes: 256
} as const
export const MODEL_BRIDGE_PROTOCOL = 'goodbuddy-model-bridge-v1' as const
export const modelBridgeModelProtocolSchema = z.enum([
'anthropic-messages',
'openai-responses',
'openai-chat-completions'
])
export type ModelBridgeModelProtocol = z.infer<
typeof modelBridgeModelProtocolSchema
>
const modelNameSchema = utf8StringSchema(
MODEL_BRIDGE_LIMITS.maximumModelNameBytes,
{
minimumBytes: 1,
label: 'Model bridge model name'
}
)
.refine(
(value) => value.trim() === value,
'Model name must not have outer whitespace'
)
.refine(
(value) =>
Array.from(value).every((character) => {
const codePoint = character.codePointAt(0)!
return codePoint > 0x1f && codePoint !== 0x7f
}),
'Model name cannot contain control characters'
)
export const modelBridgePolicySchema = z
.object({
protocol: modelBridgeModelProtocolSchema,
model: modelNameSchema,
modelProfileDigest: sha256DigestSchema,
// Accepted only so persisted pre-removal bindings can be read and
// retired normally. Current code neither emits nor enforces these fields.
maximumOutputTokens: z.number().int().min(1).optional(),
maximumModelCalls: z.number().int().min(1).optional(),
maximumTotalOutputTokens: z.number().int().min(1).optional(),
supportsImageInput: z.boolean()
})
.strict()
export type ModelBridgePolicy = z.infer<
typeof modelBridgePolicySchema
>
export const modelBridgeIdentitySchema = z
.object({
bindingId: agentIdentifierSchema,
promptOperationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
roundIndex: z
.number()
.int()
.nonnegative()
.max(Number.MAX_SAFE_INTEGER),
modelProfileDigest: sha256DigestSchema,
messageId: agentIdentifierSchema
})
.strict()
export type ModelBridgeIdentity = z.infer<
typeof modelBridgeIdentitySchema
>
const modelBridgeRequestMessageBaseSchema = z
.object({
protocol: z.literal(MODEL_BRIDGE_PROTOCOL),
kind: z.literal('request'),
identity: modelBridgeIdentitySchema,
policy: modelBridgePolicySchema,
requestDigest: sha256DigestSchema,
request: remoteModelGatewayRequestSchema
})
.strict()
export const modelBridgeRequestMessageSchema =
modelBridgeRequestMessageBaseSchema.superRefine((message, context) => {
if (
message.identity.modelProfileDigest !==
message.policy.modelProfileDigest
) {
context.addIssue({
code: 'custom',
path: ['policy', 'modelProfileDigest'],
message: 'Policy model profile digest does not match the message identity'
})
}
if (!pathMatchesProtocol(message.policy.protocol, message.request.path)) {
context.addIssue({
code: 'custom',
path: ['request', 'path'],
message: 'Provider path does not match the model bridge policy protocol'
})
}
})
export type ModelBridgeRequestMessage = z.infer<
typeof modelBridgeRequestMessageSchema
>
export const modelBridgeResponseMessageSchema = z
.object({
protocol: z.literal(MODEL_BRIDGE_PROTOCOL),
kind: z.literal('response'),
identity: modelBridgeIdentitySchema,
requestDigest: sha256DigestSchema,
response: remoteModelGatewayResponseSchema
})
.strict()
export type ModelBridgeResponseMessage = z.infer<
typeof modelBridgeResponseMessageSchema
>
export const modelBridgeErrorSchema = z
.object({
code: agentIdentifierSchema,
message: utf8StringSchema(
MODEL_BRIDGE_LIMITS.maximumErrorMessageBytes,
{
minimumBytes: 1,
label: 'Model bridge error message'
}
),
retryable: z.boolean(),
poisoned: z.boolean(),
outcomeUnknown: z.boolean()
})
.strict()
export type ModelBridgeError = z.infer<
typeof modelBridgeErrorSchema
>
export const modelBridgeErrorMessageSchema = z
.object({
protocol: z.literal(MODEL_BRIDGE_PROTOCOL),
kind: z.literal('error'),
identity: modelBridgeIdentitySchema,
requestDigest: sha256DigestSchema,
error: modelBridgeErrorSchema
})
.strict()
export type ModelBridgeErrorMessage = z.infer<
typeof modelBridgeErrorMessageSchema
>
export const modelBridgeDeliveryAckMessageSchema = z
.object({
protocol: z.literal(MODEL_BRIDGE_PROTOCOL),
kind: z.literal('response-delivered'),
identity: modelBridgeIdentitySchema,
requestDigest: sha256DigestSchema
})
.strict()
export type ModelBridgeDeliveryAckMessage = z.infer<
typeof modelBridgeDeliveryAckMessageSchema
>
export const modelBridgeMessageSchema = z.discriminatedUnion('kind', [
modelBridgeRequestMessageSchema,
modelBridgeResponseMessageSchema,
modelBridgeErrorMessageSchema,
modelBridgeDeliveryAckMessageSchema
])
export type ModelBridgeMessage = z.infer<
typeof modelBridgeMessageSchema
>
export type ModelBridgeCodecErrorCode =
| 'digest-mismatch'
| 'identity-mismatch'
| 'invalid-message'
| 'oversized'
export class ModelBridgeCodecError extends Error {
readonly code: ModelBridgeCodecErrorCode
constructor(code: ModelBridgeCodecErrorCode, message: string) {
super(message)
this.name = 'ModelBridgeCodecError'
this.code = code
}
}
export async function digestModelBridgeRequest(
request: unknown
): Promise<string> {
const parsed = remoteModelGatewayRequestSchema.parse(request)
return sha256Digest(
canonicalJson({
protocol: MODEL_BRIDGE_PROTOCOL,
request: parsed
})
)
}
export async function createModelBridgeRequestMessage(input: {
identity: unknown
policy: unknown
request: unknown
}): Promise<ModelBridgeRequestMessage> {
const identity = modelBridgeIdentitySchema.parse(input.identity)
const policy = modelBridgePolicySchema.parse(input.policy)
const request = remoteModelGatewayRequestSchema.parse(input.request)
return modelBridgeRequestMessageSchema.parse({
protocol: MODEL_BRIDGE_PROTOCOL,
kind: 'request',
identity,
policy,
requestDigest: await digestModelBridgeRequest(request),
request
})
}
export async function assertModelBridgeRequestDigest(
message: ModelBridgeRequestMessage
): Promise<void> {
const actualDigest = await digestModelBridgeRequest(message.request)
if (actualDigest !== message.requestDigest) {
throw new ModelBridgeCodecError(
'digest-mismatch',
'Model bridge request digest does not match the exact request'
)
}
}
export async function encodeModelBridgeMessage(
input: unknown
): Promise<Uint8Array> {
const message = modelBridgeMessageSchema.parse(input)
if (message.kind === 'request') {
await assertModelBridgeRequestDigest(message)
}
const bytes = textEncoder.encode(canonicalJson(message))
if (bytes.byteLength > MODEL_BRIDGE_LIMITS.maximumMessageBytes) {
throw new ModelBridgeCodecError(
'oversized',
'Model bridge message exceeds its byte limit'
)
}
return bytes
}
export type ModelBridgeDecodeOptions = {
expectedIdentity?: ModelBridgeIdentity
expectedRequestDigest?: string
maximumMessageBytes?: number
}
export async function decodeModelBridgeMessage(
input: Uint8Array,
options: ModelBridgeDecodeOptions = {}
): Promise<ModelBridgeMessage> {
const maximumMessageBytes =
options.maximumMessageBytes ?? MODEL_BRIDGE_LIMITS.maximumMessageBytes
if (
!Number.isSafeInteger(maximumMessageBytes) ||
maximumMessageBytes < 1 ||
maximumMessageBytes > MODEL_BRIDGE_LIMITS.maximumMessageBytes
) {
throw new RangeError('Invalid model bridge message limit')
}
if (input.byteLength < 1 || input.byteLength > maximumMessageBytes) {
throw new ModelBridgeCodecError(
'oversized',
'Model bridge message exceeds its configured byte limit'
)
}
let text: string
let unparsed: unknown
try {
text = fatalTextDecoder.decode(input)
unparsed = JSON.parse(text) as unknown
} catch {
throw new ModelBridgeCodecError(
'invalid-message',
'Model bridge message is not canonical UTF-8 JSON'
)
}
let canonical: string
try {
canonical = canonicalJson(unparsed)
} catch {
throw new ModelBridgeCodecError(
'invalid-message',
'Model bridge message is not canonical JSON'
)
}
if (canonical !== text) {
throw new ModelBridgeCodecError(
'invalid-message',
'Model bridge message JSON is not canonical'
)
}
const parsed = modelBridgeMessageSchema.safeParse(unparsed)
if (!parsed.success) {
throw new ModelBridgeCodecError(
'invalid-message',
'Model bridge message does not match the wire contract'
)
}
const message = parsed.data
const expectedIdentity =
options.expectedIdentity === undefined
? undefined
: modelBridgeIdentitySchema.parse(options.expectedIdentity)
if (
expectedIdentity !== undefined &&
!sameIdentity(message.identity, expectedIdentity)
) {
throw new ModelBridgeCodecError(
'identity-mismatch',
'Model bridge message identity does not match the expected request'
)
}
const expectedRequestDigest =
options.expectedRequestDigest === undefined
? undefined
: sha256DigestSchema.parse(options.expectedRequestDigest)
if (
expectedRequestDigest !== undefined &&
message.requestDigest !== expectedRequestDigest
) {
throw new ModelBridgeCodecError(
'digest-mismatch',
'Model bridge message does not match the expected request digest'
)
}
if (message.kind === 'request') {
await assertModelBridgeRequestDigest(message)
}
return message
}
function sameIdentity(
left: ModelBridgeIdentity,
right: ModelBridgeIdentity
): boolean {
return (
left.bindingId === right.bindingId &&
left.promptOperationId === right.promptOperationId &&
left.requestId === right.requestId &&
left.roundIndex === right.roundIndex &&
left.modelProfileDigest === right.modelProfileDigest &&
left.messageId === right.messageId
)
}
function pathMatchesProtocol(
protocol: ModelBridgeModelProtocol,
path: string
): boolean {
switch (protocol) {
case 'anthropic-messages':
return path === '/v1/messages'
case 'openai-chat-completions':
return (
path === '/chat/completions' ||
path === '/v1/chat/completions'
)
case 'openai-responses':
return path === '/responses' || path === '/v1/responses'
}
}
async function sha256Digest(value: string): Promise<string> {
const digest = await globalThis.crypto.subtle.digest(
'SHA-256',
textEncoder.encode(value)
)
return `sha256:${Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, '0')
).join('')}`
}
@@ -0,0 +1,135 @@
import { describe, expect, it } from 'vitest'
import {
completeModelCallEvidenceSchema,
failModelCallEvidenceSchema,
modelCallDispatchMetadataSchema,
modelCallRecordSchema,
prepareModelCallSchema
} from './model-call-operation-contracts'
const identity = {
callOperationId: 'call-1',
requestId: 'request-1',
bindingId: 'binding-1',
promptOperationId: 'prompt-1',
promptSequence: 0,
roundIndex: 0,
provider: 'provider',
profile: 'default',
model: 'model',
protocol: 'responses-v1'
}
const policy = {
modelProfileDigest: `sha256:${'b'.repeat(64)}`
}
describe('model call operation contracts', () => {
it('rejects secret or payload fields at every persistence boundary', () => {
const digest = `sha256:${'a'.repeat(64)}`
expect(() =>
prepareModelCallSchema.parse({
identity,
requestDigest: digest,
...policy,
prompt: 'secret prompt'
})
).toThrow()
expect(() =>
prepareModelCallSchema.parse({
identity: { ...identity, apiKey: 'secret-key' },
requestDigest: digest,
...policy
})
).toThrow()
expect(() =>
modelCallDispatchMetadataSchema.parse({
providerIdempotencyKey: 'safe-id',
requestBody: { messages: [] }
})
).toThrow()
expect(() =>
completeModelCallEvidenceSchema.parse({
status: 'completed',
providerResponseId: 'response-1',
responseBody: 'secret response'
})
).toThrow()
expect(() =>
failModelCallEvidenceSchema.parse({
status: 'failed-definitive',
error: {
code: 'authentication',
retryable: false,
message: 'credential was secret-value'
}
})
).toThrow()
})
it('bounds provider identifiers and safe terminal metadata', () => {
expect(() =>
modelCallDispatchMetadataSchema.parse({
providerRequestId: 'x'.repeat(257)
})
).toThrow()
expect(() =>
completeModelCallEvidenceSchema.parse({
status: 'completed',
result: { finishReason: 'x'.repeat(129) }
})
).toThrow()
})
it('tags terminal evidence and enforces record state consistency', () => {
const base = {
identity,
requestDigest: `sha256:${'a'.repeat(64)}`,
...policy,
preparedAt: 1,
updatedAt: 3,
dispatchedAt: 2,
dispatchMetadata: {}
}
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'completed',
terminalAt: 3,
terminalEvidence: {
status: 'failed-definitive',
error: { code: 'authentication', retryable: false }
}
})
).toThrow()
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'prepared'
})
).toThrow()
expect(() =>
modelCallRecordSchema.parse({
...base,
status: 'dispatched',
updatedAt: 1
})
).toThrow()
})
it('accepts legacy quota fields without requiring them', () => {
const request = {
identity,
requestDigest: `sha256:${'a'.repeat(64)}`,
...policy
}
expect(prepareModelCallSchema.parse(request)).toEqual(request)
expect(
prepareModelCallSchema.parse({
...request,
maximumOutputTokens: 65_537,
maximumModelCalls: 100_000,
maximumTotalOutputTokens: 1_000_000_000
})
).toMatchObject(request)
})
})
@@ -0,0 +1,323 @@
import { z } from 'zod'
import {
agentIdentifierSchema,
sha256DigestSchema,
utf8StringSchema
} from './agent-protocol/contracts'
const countSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
const positiveCountSchema = countSchema.positive()
const timestampSchema = countSchema
const providerLabelSchema = utf8StringSchema(256, {
minimumBytes: 1,
label: 'Model provider label'
}).refine((value) => value.trim() === value, 'Label must not have outer whitespace')
const providerIdentifierSchema = utf8StringSchema(256, {
minimumBytes: 1,
label: 'Provider identifier'
}).refine(
(value) => value.trim() === value,
'Provider identifier must not have outer whitespace'
)
export const modelCallDigestSchema = sha256DigestSchema
export const modelCallStatusSchema = z.enum([
'prepared',
'dispatched',
'completed',
'failed-definitive',
'outcome-unknown'
])
export type ModelCallStatus = z.infer<typeof modelCallStatusSchema>
export const modelCallIdentitySchema = z
.object({
callOperationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
promptOperationId: agentIdentifierSchema,
promptSequence: z
.number()
.int()
.nonnegative()
.max(Number.MAX_SAFE_INTEGER)
.default(0),
roundIndex: z
.number()
.int()
.nonnegative()
.max(Number.MAX_SAFE_INTEGER),
provider: providerLabelSchema,
profile: providerLabelSchema,
model: providerLabelSchema,
protocol: providerLabelSchema
})
.strict()
export type ModelCallIdentity = z.infer<typeof modelCallIdentitySchema>
export const prepareModelCallSchema = z
.object({
identity: modelCallIdentitySchema,
requestDigest: modelCallDigestSchema,
modelProfileDigest: modelCallDigestSchema,
// Legacy ledger fields are accepted during migration but no longer
// constrain dispatch.
maximumOutputTokens: positiveCountSchema.optional(),
maximumModelCalls: positiveCountSchema.optional(),
maximumTotalOutputTokens: positiveCountSchema.optional()
})
.strict()
export type PrepareModelCall = z.infer<typeof prepareModelCallSchema>
export const modelCallDispatchMetadataSchema = z
.object({
providerIdempotencyKey: providerIdentifierSchema.optional(),
providerRequestId: providerIdentifierSchema.optional()
})
.strict()
export type ModelCallDispatchMetadata = z.infer<
typeof modelCallDispatchMetadataSchema
>
export const modelCallUsageMetadataSchema = z
.object({
inputTokens: countSchema.optional(),
outputTokens: countSchema.optional(),
cachedInputTokens: countSchema.optional(),
reasoningTokens: countSchema.optional()
})
.strict()
export type ModelCallUsageMetadata = z.infer<
typeof modelCallUsageMetadataSchema
>
export const modelCallResultMetadataSchema = z
.object({
finishReason: agentIdentifierSchema.optional(),
outputDigest: modelCallDigestSchema.optional(),
artifactCount: countSchema.max(1_000).optional()
})
.strict()
export type ModelCallResultMetadata = z.infer<
typeof modelCallResultMetadataSchema
>
export const modelCallErrorMetadataSchema = z
.object({
code: agentIdentifierSchema,
retryable: z.boolean()
})
.strict()
export type ModelCallErrorMetadata = z.infer<
typeof modelCallErrorMetadataSchema
>
export const completeModelCallEvidenceSchema = z
.object({
status: z.literal('completed'),
providerRequestId: providerIdentifierSchema.optional(),
providerResponseId: providerIdentifierSchema.optional(),
result: modelCallResultMetadataSchema.optional(),
usage: modelCallUsageMetadataSchema.optional()
})
.strict()
export type CompleteModelCallEvidence = z.infer<
typeof completeModelCallEvidenceSchema
>
export const failModelCallEvidenceSchema = z
.object({
status: z.literal('failed-definitive'),
providerRequestId: providerIdentifierSchema.optional(),
providerResponseId: providerIdentifierSchema.optional(),
error: modelCallErrorMetadataSchema,
usage: modelCallUsageMetadataSchema.optional()
})
.strict()
export type FailModelCallEvidence = z.infer<
typeof failModelCallEvidenceSchema
>
export const unknownModelCallEvidenceSchema = z
.object({
status: z.literal('outcome-unknown'),
providerRequestId: providerIdentifierSchema.optional(),
reason: modelCallErrorMetadataSchema
})
.strict()
export type UnknownModelCallEvidence = z.infer<
typeof unknownModelCallEvidenceSchema
>
export const modelCallTerminalEvidenceSchema = z.discriminatedUnion('status', [
completeModelCallEvidenceSchema,
failModelCallEvidenceSchema,
unknownModelCallEvidenceSchema
])
export type ModelCallTerminalEvidence = z.infer<
typeof modelCallTerminalEvidenceSchema
>
export const modelCallRecordSchema = z
.object({
identity: modelCallIdentitySchema,
requestDigest: modelCallDigestSchema,
modelProfileDigest: modelCallDigestSchema,
maximumOutputTokens: positiveCountSchema.optional(),
maximumModelCalls: positiveCountSchema.optional(),
maximumTotalOutputTokens: positiveCountSchema.optional(),
status: modelCallStatusSchema,
dispatchMetadata: modelCallDispatchMetadataSchema.optional(),
terminalEvidence: modelCallTerminalEvidenceSchema.optional(),
preparedAt: timestampSchema,
updatedAt: timestampSchema,
dispatchedAt: timestampSchema.optional(),
terminalAt: timestampSchema.optional(),
responseDeliveredAt: timestampSchema.optional()
})
.strict()
.superRefine((record, context) => {
if (record.updatedAt < record.preparedAt) {
addRecordIssue(context, ['updatedAt'], 'updatedAt precedes preparedAt')
}
const hasDispatch =
record.dispatchMetadata !== undefined && record.dispatchedAt !== undefined
if (
(record.dispatchMetadata === undefined) !==
(record.dispatchedAt === undefined)
) {
addRecordIssue(
context,
['dispatchMetadata'],
'dispatch metadata and timestamp must appear together'
)
}
if (
record.dispatchedAt !== undefined &&
(record.dispatchedAt < record.preparedAt ||
record.updatedAt < record.dispatchedAt)
) {
addRecordIssue(
context,
['dispatchedAt'],
'dispatchedAt is outside the record timestamp range'
)
}
if (record.status === 'prepared') {
if (
hasDispatch ||
record.terminalEvidence ||
record.terminalAt !== undefined ||
record.responseDeliveredAt !== undefined
) {
addRecordIssue(
context,
['status'],
'prepared records cannot contain dispatch or terminal state'
)
}
return
}
if (!hasDispatch) {
addRecordIssue(
context,
['dispatchMetadata'],
'dispatched and terminal records require dispatch state'
)
}
if (record.status === 'dispatched') {
if (
record.terminalEvidence ||
record.terminalAt !== undefined ||
record.responseDeliveredAt !== undefined
) {
addRecordIssue(
context,
['status'],
'dispatched records cannot contain terminal state'
)
}
return
}
if (
record.terminalEvidence?.status !== record.status ||
record.terminalAt === undefined
) {
addRecordIssue(
context,
['terminalEvidence'],
'terminal evidence and timestamp must match record status'
)
}
if (
record.terminalAt !== undefined &&
(record.dispatchedAt === undefined ||
record.terminalAt < record.dispatchedAt ||
record.updatedAt < record.terminalAt)
) {
addRecordIssue(
context,
['terminalAt'],
'terminalAt is outside the record timestamp range'
)
}
if (
record.responseDeliveredAt !== undefined &&
(record.status !== 'completed' ||
record.terminalAt === undefined ||
record.responseDeliveredAt < record.terminalAt ||
record.updatedAt < record.responseDeliveredAt)
) {
addRecordIssue(
context,
['responseDeliveredAt'],
'responseDeliveredAt requires a completed provider response'
)
}
})
export type ModelCallRecord = z.infer<typeof modelCallRecordSchema>
export const modelCallOperationCursorSchema = z
.object({
updatedAt: timestampSchema,
callOperationId: agentIdentifierSchema
})
.strict()
export type ModelCallOperationCursor = z.infer<
typeof modelCallOperationCursorSchema
>
export const modelCallDeliveryFilterSchema = z.enum([
'any',
'pending-response',
'delivered'
])
export type ModelCallDeliveryFilter = z.infer<
typeof modelCallDeliveryFilterSchema
>
export const modelCallListOptionsSchema = z
.object({
statuses: z.array(modelCallStatusSchema).min(1).max(5).optional(),
delivery: modelCallDeliveryFilterSchema.default('any'),
cursor: modelCallOperationCursorSchema.optional(),
limit: positiveCountSchema.max(1_000).default(100)
})
.strict()
export type ModelCallListOptions = z.input<
typeof modelCallListOptionsSchema
>
export type ModelCallOperationPage = {
records: ModelCallRecord[]
nextCursor?: ModelCallOperationCursor
}
function addRecordIssue(
context: z.RefinementCtx,
path: Array<string | number>,
message: string
): void {
context.addIssue({ code: 'custom', path, message })
}
+168
View File
@@ -0,0 +1,168 @@
import {
closeSync,
constants,
fchmodSync,
fstatSync,
lstatSync,
openSync,
type BigIntStats
} from 'node:fs'
import { dirname, resolve } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
function assertSameFile(
descriptorStats: BigIntStats,
pathStats: BigIntStats
): void {
if (
!pathStats.isFile() ||
pathStats.isSymbolicLink() ||
descriptorStats.dev !== pathStats.dev ||
descriptorStats.ino !== pathStats.ino
) {
throw new Error('SQLite database path identity changed during open')
}
}
/**
* Verifies a local same-UID security boundary. On POSIX the database and its
* immediate parent must be owned by the current UID, the parent must be
* private, and the database mode is forced to 0600. This does not defend
* against a process already running as the same UID.
*/
export class PreparedPrivateSqliteDatabaseFile {
private descriptor?: number
readonly path: string
constructor(databasePath: string) {
if (
databasePath.length === 0 ||
databasePath === ':memory:' ||
/^file:/iu.test(databasePath)
) {
throw new Error('SQLite database must be a filesystem path')
}
this.path = resolve(databasePath)
const parentStats = lstatSync(dirname(this.path), { bigint: true })
if (parentStats.isSymbolicLink() || !parentStats.isDirectory()) {
throw new Error('SQLite database parent must be a real directory')
}
this.assertPrivatePosixParent(parentStats)
try {
const pathStats = lstatSync(this.path, { bigint: true })
if (pathStats.isSymbolicLink() || !pathStats.isFile()) {
throw new Error(
'SQLite database must be a regular, non-symbolic-link file'
)
}
} catch (error) {
if (!isMissingFileError(error)) {
throw error
}
}
this.descriptor = openSync(
this.path,
constants.O_CREAT |
constants.O_RDWR |
(constants.O_NOFOLLOW ?? 0),
0o600
)
try {
if (process.platform !== 'win32') {
fchmodSync(this.descriptor, 0o600)
}
this.verifyPathIdentity()
} catch (error) {
this.close()
throw error
}
}
openDatabase(timeout = 5_000): DatabaseSync {
this.verifyPathIdentity()
let database: DatabaseSync | undefined
try {
database = new DatabaseSync(this.path, { timeout })
this.verifyPathIdentity()
return database
} catch (error) {
database?.close()
throw error
} finally {
this.close()
}
}
close(): void {
if (this.descriptor !== undefined) {
closeSync(this.descriptor)
this.descriptor = undefined
}
}
private verifyPathIdentity(): void {
if (this.descriptor === undefined) {
throw new Error('SQLite database verification descriptor is closed')
}
const descriptorStats = fstatSync(this.descriptor, { bigint: true })
const pathStats = lstatSync(this.path, { bigint: true })
assertSameFile(descriptorStats, pathStats)
this.assertPrivatePosixFile(descriptorStats)
}
private assertPrivatePosixParent(parentStats: BigIntStats): void {
if (process.platform === 'win32') {
return
}
const uid = process.getuid?.()
if (
uid === undefined ||
parentStats.uid !== BigInt(uid) ||
(parentStats.mode & 0o077n) !== 0n
) {
throw new Error(
'SQLite database parent must be private and owned by the current UID'
)
}
}
private assertPrivatePosixFile(fileStats: BigIntStats): void {
if (process.platform === 'win32') {
return
}
const uid = process.getuid?.()
if (
uid === undefined ||
fileStats.uid !== BigInt(uid) ||
(fileStats.mode & 0o777n) !== 0o600n
) {
throw new Error(
'SQLite database must be mode 0600 and owned by the current UID'
)
}
}
}
export function openPrivateSqliteDatabase(
databasePath: string,
timeout = 5_000
): DatabaseSync {
const prepared = new PreparedPrivateSqliteDatabaseFile(databasePath)
try {
return prepared.openDatabase(timeout)
} finally {
prepared.close()
}
}
function isMissingFileError(error: unknown): boolean {
return (
error !== null &&
typeof error === 'object' &&
'code' in error &&
error.code === 'ENOENT'
)
}
+612
View File
@@ -0,0 +1,612 @@
import {
createHash,
createPublicKey,
verify
} from 'node:crypto'
import { createReadStream, lstatSync } from 'node:fs'
import {
lstat,
open,
readFile,
readdir
} from 'node:fs/promises'
import {
basename,
isAbsolute,
join,
parse,
relative,
resolve,
sep
} from 'node:path'
import {
agentReleaseKeyRegistrySchema,
type AgentArchitecture,
type AgentReleaseKey,
type AgentReleaseKeyRegistry
} from '../agent-installation-contracts'
import {
assertDetachedRemoteRuntimeBundleDigest,
digestRemoteRuntimeBundleManifest,
remoteRuntimeBundleManifestSchema,
remoteRuntimeLockSchema,
type RemoteRuntimeBundleManifest,
type RemoteRuntimeLock
} from '../remote-runtime-launch-contracts'
export type InstalledBundleVerificationEnvironment =
| 'production'
| 'test'
const SIGNATURE_DOMAIN = Buffer.from(
'GoodBuddy Remote Runtime Bundle Manifest Signature v1\0',
'utf8'
)
const MANIFEST_FILE_NAME = 'manifest.json'
const SIGNATURE_FILE_NAME = 'manifest.sig'
const MAXIMUM_METADATA_BYTES = 1024 * 1024
export type VerifiedRuntimeBundle = {
bundleDirectory: string
executablePath: string
manifest: RemoteRuntimeBundleManifest
manifestDigest: string
}
export type VerifyRuntimeBundleOptions = {
architecture: AgentArchitecture
releaseKeyRegistry: AgentReleaseKeyRegistry
runtimeLock: RemoteRuntimeLock
verificationEnvironment?: InstalledBundleVerificationEnvironment
enforceFilesystemMode?: boolean
filesystemPlatform?: NodeJS.Platform
uid?: number
}
export async function verifyRuntimeBundle(
bundleDirectoryInput: string,
options: VerifyRuntimeBundleOptions
): Promise<VerifiedRuntimeBundle> {
const bundleDirectory = assertAbsoluteManagedPath(
resolve(bundleDirectoryInput)
)
const registry = parseReleaseKeyRegistry(
options.releaseKeyRegistry
)
const runtimeLock = remoteRuntimeLockSchema.parse(options.runtimeLock)
const verificationEnvironment =
options.verificationEnvironment ?? 'production'
assertRegistryAvailable(registry, verificationEnvironment)
const rootStat = await lstat(bundleDirectory)
assertOwnedDirectory(rootStat, 'Runtime bundle directory', options.uid)
if (
shouldEnforceMode(options) &&
modeString(rootStat.mode) !== '0700'
) {
throw new Error('Runtime bundle directory permissions must be 0700')
}
const manifestBytes = await readMetadataFile(
join(bundleDirectory, MANIFEST_FILE_NAME),
'Runtime manifest',
options
)
const signatureBytes = await readSignature(
join(bundleDirectory, SIGNATURE_FILE_NAME),
options
)
const manifest = await verifyRuntimeManifestSignature(
manifestBytes,
signatureBytes,
registry,
verificationEnvironment
)
assertRuntimeManifestMatchesLock(
manifest,
runtimeLock,
options.architecture
)
if (
basename(bundleDirectory) !==
manifest.bundleDigest.slice('sha256:'.length)
) {
throw new Error(
'Runtime bundle digest does not match its managed directory'
)
}
const inventory = await inventoryBundleFiles(
bundleDirectory,
options.uid
)
const declaredPaths = new Set(
manifest.files.map((file) => file.path)
)
for (const file of manifest.files) {
const filePath = join(
bundleDirectory,
...file.path.split('/')
)
const stat = inventory.get(file.path)
if (stat === undefined) {
throw new Error(`Runtime payload is missing: ${file.path}`)
}
if (stat.size !== file.size) {
throw new Error(`Runtime payload size mismatch: ${file.path}`)
}
if ((await sha256File(filePath)) !== file.sha256) {
throw new Error(`Runtime payload hash mismatch: ${file.path}`)
}
if (
shouldEnforceMode(options) &&
modeString(stat.mode) !== file.mode
) {
throw new Error(`Runtime payload mode mismatch: ${file.path}`)
}
}
const actualPaths = new Set(inventory.keys())
const expectedPaths = new Set([
...declaredPaths,
MANIFEST_FILE_NAME,
SIGNATURE_FILE_NAME
])
if (
actualPaths.size !== expectedPaths.size ||
[...actualPaths].some((path) => !expectedPaths.has(path))
) {
throw new Error(
'Runtime bundle contains undeclared or missing files'
)
}
const executablePath = join(
bundleDirectory,
...manifest.entrypoint.path.split('/')
)
await assertElfArchitecture(
executablePath,
options.architecture,
'Runtime executable'
)
return {
bundleDirectory,
executablePath,
manifest,
manifestDigest:
await digestRemoteRuntimeBundleManifest(manifest)
}
}
export function canonicalRuntimeManifestBytes(
manifest: RemoteRuntimeBundleManifest
): Buffer {
return Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
}
export function runtimeManifestSignaturePayload(
manifestBytes: Uint8Array
): Buffer {
return Buffer.concat([
SIGNATURE_DOMAIN,
Buffer.from(manifestBytes)
])
}
export async function verifyRuntimeManifestSignature(
manifestBytes: Buffer,
signatureBytes: Buffer,
registryInput: AgentReleaseKeyRegistry,
verificationEnvironment: InstalledBundleVerificationEnvironment =
'production'
): Promise<RemoteRuntimeBundleManifest> {
const registry = parseReleaseKeyRegistry(registryInput)
assertRegistryAvailable(registry, verificationEnvironment)
let untrusted: unknown
try {
untrusted = JSON.parse(manifestBytes.toString('utf8'))
} catch (error) {
throw new Error('Runtime manifest JSON is invalid', { cause: error })
}
const parsed = remoteRuntimeBundleManifestSchema.safeParse(untrusted)
if (!parsed.success) {
throw new Error(
`Runtime manifest contract is invalid: ${parsed.error.message}`
)
}
const manifest = parsed.data
if (!canonicalRuntimeManifestBytes(manifest).equals(manifestBytes)) {
throw new Error(
'Runtime manifest is not in canonical deterministic form'
)
}
const key = trustedKeyForManifest(
manifest,
registry,
verificationEnvironment
)
const publicKey = importPublicKey(key)
if (
signatureBytes.length !== 64 ||
!verify(
null,
runtimeManifestSignaturePayload(manifestBytes),
publicKey,
signatureBytes
)
) {
throw new Error('Runtime manifest signature verification failed')
}
await assertDetachedRemoteRuntimeBundleDigest({
manifest,
detachedSignature: {
manifestDigest:
await digestRemoteRuntimeBundleManifest(manifest),
algorithm: 'ed25519',
keyId: manifest.signingKeyId,
signatureBase64: signatureBytes.toString('base64')
}
})
return manifest
}
export async function readRemoteRuntimeLock(
filePathInput: string,
options: {
uid?: number
filesystemPlatform?: NodeJS.Platform
} = {}
): Promise<RemoteRuntimeLock> {
const filePath = assertAbsoluteManagedPath(resolve(filePathInput))
const contents = await readMetadataFile(
filePath,
'Remote Runtime lock',
options
)
let value: unknown
try {
value = JSON.parse(contents.toString('utf8'))
} catch (error) {
throw new Error('Remote Runtime lock is invalid JSON', {
cause: error
})
}
return remoteRuntimeLockSchema.parse(value)
}
export function assertRuntimeManifestMatchesLock(
manifest: RemoteRuntimeBundleManifest,
lock: RemoteRuntimeLock,
architecture: AgentArchitecture
): void {
const expected = lock.runtimes.opencode
const target = expected.targets[architecture]
if (
manifest.runtimeId !== 'opencode' ||
manifest.provider !== expected.provider ||
manifest.runtimeVersion !== expected.version ||
manifest.architecture !== architecture ||
manifest.entrypoint.path !== expected.entrypoint ||
manifest.entrypoint.identity !== expected.entrypointIdentity ||
manifest.entrypoint.argvPrefix.length !==
expected.argvPrefix.length ||
manifest.entrypoint.argvPrefix.some(
(argument, index) => argument !== expected.argvPrefix[index]
) ||
manifest.protocol.major !== expected.protocol.major ||
manifest.protocol.minor !== expected.protocol.minor ||
manifest.allowedEnvironmentNames.length !==
expected.allowedEnvironmentNames.length ||
manifest.allowedEnvironmentNames.some(
(name, index) =>
name !== expected.allowedEnvironmentNames[index]
) ||
manifest.sourcePackage.name !== target.package ||
manifest.sourcePackage.integrity !== target.integrity
) {
throw new Error(
'Runtime manifest does not match the locked OpenCode profile'
)
}
}
function parseReleaseKeyRegistry(
value: unknown
): AgentReleaseKeyRegistry {
const parsed = agentReleaseKeyRegistrySchema.safeParse(value)
if (!parsed.success) {
throw new Error(
`Runtime trusted key registry contract is invalid: ${parsed.error.message}`
)
}
for (const key of parsed.data.keys) {
importPublicKey(key)
}
return parsed.data
}
function assertRegistryAvailable(
registry: AgentReleaseKeyRegistry,
environment: InstalledBundleVerificationEnvironment
): void {
if (
environment === 'production' &&
!registry.keys.some((key) => key.environment === 'production')
) {
throw new Error(
'Runtime production key registry is empty; verification is unavailable'
)
}
}
function trustedKeyForManifest(
manifest: RemoteRuntimeBundleManifest,
registry: AgentReleaseKeyRegistry,
environment: InstalledBundleVerificationEnvironment
): AgentReleaseKey {
const key = registry.keys.find(
(candidate) => candidate.keyId === manifest.signingKeyId
)
if (key === undefined) {
throw new Error(
`Runtime manifest uses an unknown signing key: ${manifest.signingKeyId}`
)
}
if (environment === 'production' && key.environment !== 'production') {
throw new Error(
`Production Runtime verification rejects non-production key: ${key.keyId}`
)
}
if (
registry.revocations.some(
(revocation) => revocation.keyId === key.keyId
)
) {
throw new Error(`Runtime signing key is revoked: ${key.keyId}`)
}
return key
}
function importPublicKey(key: AgentReleaseKey) {
let publicKey
try {
publicKey = createPublicKey({
key: Buffer.from(key.publicKeySpkiBase64, 'base64'),
format: 'der',
type: 'spki'
})
} catch (error) {
throw new Error(
`Runtime trusted public key is invalid: ${key.keyId}`,
{ cause: error }
)
}
if (publicKey.asymmetricKeyType !== 'ed25519') {
throw new Error(
`Runtime trusted public key is not Ed25519: ${key.keyId}`
)
}
return publicKey
}
async function readMetadataFile(
filePath: string,
label: string,
options: {
enforceFilesystemMode?: boolean
filesystemPlatform?: NodeJS.Platform
uid?: number
}
): Promise<Buffer> {
const stat = await lstat(filePath)
assertOwnedRegularFile(stat, label, options.uid)
if (
shouldEnforceMode(options) &&
modeString(stat.mode) !== '0644' &&
modeString(stat.mode) !== '0600'
) {
throw new Error(`${label} permissions must be 0600 or 0644`)
}
if (stat.size > MAXIMUM_METADATA_BYTES) {
throw new Error(`${label} exceeds its size limit`)
}
return readFile(filePath)
}
async function readSignature(
filePath: string,
options: VerifyRuntimeBundleOptions
): Promise<Buffer> {
const contents = await readMetadataFile(
filePath,
'Runtime detached signature',
options
)
const text = contents.toString('utf8')
if (!/^[A-Za-z0-9+/]{86}==\n$/u.test(text)) {
throw new Error('Runtime detached signature encoding is invalid')
}
return Buffer.from(text.trim(), 'base64')
}
async function inventoryBundleFiles(
bundleDirectory: string,
uid = process.getuid?.()
): Promise<Map<string, Awaited<ReturnType<typeof lstat>>>> {
const files = new Map<
string,
Awaited<ReturnType<typeof lstat>>
>()
const pending = [bundleDirectory]
while (pending.length > 0) {
const current = pending.pop()
if (current === undefined) {
continue
}
for (const entry of await readdir(current, {
withFileTypes: true
})) {
const absolutePath = join(current, entry.name)
if (entry.isSymbolicLink()) {
throw new Error(
`Runtime bundle cannot contain a symlink: ${absolutePath}`
)
}
const stat = await lstat(absolutePath)
if (entry.isDirectory()) {
assertOwnedDirectory(
stat,
absolutePath,
uid
)
pending.push(absolutePath)
} else if (entry.isFile()) {
assertOwnedRegularFile(
stat,
absolutePath,
uid
)
files.set(
relative(bundleDirectory, absolutePath)
.split(sep)
.join('/'),
stat
)
} else {
throw new Error(
`Runtime bundle contains an unsupported file: ${absolutePath}`
)
}
}
}
return files
}
function assertOwnedDirectory(
stat: Awaited<ReturnType<typeof lstat>>,
label: string,
uid = process.getuid?.()
): void {
if (!stat.isDirectory() || stat.isSymbolicLink()) {
throw new Error(`${label} is not a real directory`)
}
if (uid !== undefined && stat.uid !== uid) {
throw new Error(`${label} is not owned by the current user`)
}
}
function assertOwnedRegularFile(
stat: Awaited<ReturnType<typeof lstat>>,
label: string,
uid = process.getuid?.()
): void {
if (!stat.isFile() || stat.isSymbolicLink()) {
throw new Error(`${label} is not a regular file`)
}
if (uid !== undefined && stat.uid !== uid) {
throw new Error(`${label} is not owned by the current user`)
}
}
function shouldEnforceMode(options: {
enforceFilesystemMode?: boolean
filesystemPlatform?: NodeJS.Platform
}): boolean {
return (
options.enforceFilesystemMode !== false &&
(options.filesystemPlatform ?? process.platform) !== 'win32'
)
}
function modeString(mode: number | bigint): string {
return (Number(mode) & 0o777).toString(8).padStart(4, '0')
}
function assertAbsoluteManagedPath(path: string): string {
if (
path.includes('\0') ||
!isAbsolute(path) ||
resolve(path) !== path
) {
throw new Error('Managed paths must be normalized absolute paths')
}
const root = parse(path).root
const components = relative(root, path).split(sep).filter(Boolean)
let current = root
for (const component of components) {
current = resolve(current, component)
try {
if (lstatSync(current).isSymbolicLink()) {
throw new Error('Managed paths cannot traverse a symlink')
}
} catch (error) {
if (
error instanceof Error &&
'code' in error &&
error.code === 'ENOENT'
) {
return path
}
throw error
}
}
return path
}
async function sha256File(filePath: string): Promise<string> {
const hash = createHash('sha256')
for await (const chunk of createReadStream(filePath)) {
hash.update(chunk as Buffer)
}
return hash.digest('hex')
}
async function assertElfArchitecture(
filePath: string,
expected: AgentArchitecture,
label: string
): Promise<void> {
const handle = await open(filePath, 'r')
try {
const header = Buffer.alloc(64)
const { bytesRead } = await handle.read(
header,
0,
header.length,
0
)
const actual = detectElfArchitecture(
header.subarray(0, bytesRead)
)
if (actual !== expected) {
throw new Error(
`${label} architecture mismatch: expected ${expected}, received ${actual ?? 'unknown'}`
)
}
} finally {
await handle.close()
}
}
function detectElfArchitecture(
header: Buffer
): AgentArchitecture | undefined {
if (
header.length < 20 ||
header[0] !== 0x7f ||
header.toString('ascii', 1, 4) !== 'ELF' ||
(header[5] !== 1 && header[5] !== 2)
) {
return undefined
}
const machine =
header[5] === 2
? header.readUInt16BE(18)
: header.readUInt16LE(18)
return machine === 62
? 'x64'
: machine === 183
? 'arm64'
: undefined
}
+314
View File
@@ -0,0 +1,314 @@
import { describe, expect, it } from 'vitest'
import {
REMOTE_WORKSPACE_LIMITS,
acpJournalCursorSchema,
assertRemoteWorkspaceWriteTextAtomicDigest,
createRemoteWorkspaceWriteTextAtomicRequest,
remoteAbsolutePathSchema,
remoteGitDiffResultSchema,
assertRemotePromptAcceptanceMatchesPreparation,
remotePromptOperationAcceptanceSchema,
remotePromptOperationPreparationSchema,
remoteRelativePathSchema,
remoteWorkspaceApplyChangeSetRequestSchema,
remoteWorkspaceHandleSchema,
remoteWorkspaceReadTextResultSchema,
remoteWorkspaceValidateRequestSchema,
remoteWorkspaceWriteResultSchema,
runtimeSessionBindingSchema
} from './remote-agent-contracts'
const digest = `sha256:${'a'.repeat(64)}`
const binding = {
bindingId: 'binding-1',
controllerId: 'controller-1',
controllerGeneration: 1,
conversationId: 'conversation-1',
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
workspaceIdentity: 'workspace-1',
agentInstallationId: 'installation-1',
daemonBootIdAtOpen: 'boot-1',
runtimeId: 'runtime-1',
runtimeBundleDigest: digest,
runtimeAdapterDigest: digest,
acpSessionId: 'session-1',
acpCapabilitiesDigest: digest,
state: 'ready' as const,
promptSequence: 0,
channelEpoch: '1',
lastOutboundJournaledSequence: '2',
lastOutboundDeliveredSequence: '1',
lastInboundJournaledSequence: '3',
lastMainAckSequence: '2'
}
describe('remote Agent contracts', () => {
it('accepts a consistent durable Runtime binding', () => {
expect(runtimeSessionBindingSchema.parse(binding)).toEqual(binding)
})
it('requires prompt operation identity while a prompt is running', () => {
expect(() =>
runtimeSessionBindingSchema.parse({
...binding,
state: 'prompt-running'
})
).toThrow(/operation identity/iu)
})
it('retains operation identity for reconciliation and unknown outcomes', () => {
for (const state of ['reconciling', 'outcome-unknown'] as const) {
expect(() =>
runtimeSessionBindingSchema.parse({
...binding,
state
})
).toThrow(/operation identity/iu)
expect(
runtimeSessionBindingSchema.parse({
...binding,
state,
activePromptOperationId: 'operation-1'
}).activePromptOperationId
).toBe('operation-1')
}
})
it('uses workMode as the complete prompt authorization contract', () => {
const common = {
bindingId: 'binding-1',
operationId: 'operation-1',
requestId: 'operation-1',
controllerId: 'controller-1',
controllerGeneration: 1,
connectionGeneration: 1,
channelEpoch: '1',
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
workspaceIdentity: 'workspace-1',
agentInstallationId: 'installation-1',
runtimeId: 'runtime-1',
runtimeBundleDigest: digest,
runtimeAdapterDigest: digest,
deadlineAt: '2030-01-01T00:00:00.000Z',
budget: {
maximumInputBytes: 10,
maximumOutputBytes: 100
}
}
const execute = remotePromptOperationPreparationSchema.parse({
...common,
workMode: 'execute'
})
expect(execute.workMode).toBe('execute')
expect(execute).not.toHaveProperty('trustTier')
expect(execute).not.toHaveProperty('capabilities')
expect(execute.budget).not.toHaveProperty('maximumToolCalls')
const acceptance = remotePromptOperationAcceptanceSchema.parse({
bindingId: execute.bindingId,
operationId: execute.operationId,
requestId: execute.requestId,
workMode: execute.workMode,
deadlineAt: execute.deadlineAt,
acceptedAt: '2029-01-01T00:00:00.000Z'
})
expect(() =>
assertRemotePromptAcceptanceMatchesPreparation(execute, acceptance)
).not.toThrow()
expect(() =>
assertRemotePromptAcceptanceMatchesPreparation(execute, {
...acceptance,
workMode: 'ask'
})
).toThrow(/does not match/iu)
})
it('rejects delivered and ACK cursors ahead of durable journals', () => {
expect(() =>
runtimeSessionBindingSchema.parse({
...binding,
lastOutboundDeliveredSequence: '3'
})
).toThrow(/Delivered cursor/iu)
expect(() =>
acpJournalCursorSchema.parse({
bindingId: 'binding-1',
channelEpoch: '1',
direction: 'runtime-to-main',
journaledSequence: '1',
deliveredSequence: '0',
mainAckSequence: '2'
})
).toThrow(/ACK cursor/iu)
})
it('validates UTF-8 path bytes and prevents traversal', () => {
expect(remoteAbsolutePathSchema.parse('/srv/项目')).toBe('/srv/项目')
expect(remoteRelativePathSchema.parse('')).toBe('')
expect(remoteRelativePathSchema.parse('src/文件.ts')).toBe(
'src/文件.ts'
)
for (const path of [
'../secret',
'src/../secret',
'/etc/passwd',
'C:/Windows',
'src\\secret',
'src//secret',
'src/\0secret'
]) {
expect(() => remoteRelativePathSchema.parse(path)).toThrow()
}
expect(() =>
remoteRelativePathSchema.parse(
`${'界'.repeat(
Math.floor(
REMOTE_WORKSPACE_LIMITS.maximumPathSegmentBytes / 3
) + 1
)}`
)
).toThrow(/oversized/iu)
})
it('keeps validate and handle contracts strict and capability-safe', () => {
const request = {
remoteRootPath: '/srv/project',
requestedAccess: 'read-only' as const,
requiredCapabilities: ['list', 'read-text'] as const
}
expect(remoteWorkspaceValidateRequestSchema.parse(request)).toEqual(
request
)
expect(() =>
remoteWorkspaceValidateRequestSchema.parse({
...request,
controllerId: 'controller-must-come-from-connection'
})
).toThrow()
expect(() =>
remoteWorkspaceHandleSchema.parse({
workspaceId: 'workspace-1',
workspaceIdentity: 'identity-1',
canonicalDisplayPath: '/srv/project',
access: 'read-only',
git: 'available',
capabilities: ['list', 'write-text-atomic'],
generation: 1
})
).toThrow(/Read-only/iu)
})
it('checks response byte counts instead of UTF-16 character counts', () => {
expect(
remoteWorkspaceReadTextResultSchema.parse({
relativePath: '你好.txt',
content: '你好',
offsetBytes: 0,
bytesRead: 6,
totalBytes: 6,
digest,
truncated: false
}).bytesRead
).toBe(6)
expect(() =>
remoteWorkspaceReadTextResultSchema.parse({
relativePath: '你好.txt',
content: '你好',
offsetBytes: 0,
bytesRead: 2,
totalBytes: 6,
digest,
truncated: false
})
).toThrow(/UTF-8/iu)
expect(() =>
remoteGitDiffResultSchema.parse({
repositoryIdentity: 'repository-1',
patch: '你好',
byteLength: 2,
truncated: false
})
).toThrow(/UTF-8/iu)
})
it('creates a stable write digest without accepting a controller identity', async () => {
const request = await createRemoteWorkspaceWriteTextAtomicRequest({
operationId: 'operation-1',
workspaceIdentity: 'identity-1',
workspaceId: 'workspace-1',
generation: 1,
relativePath: 'src/a.ts',
content: '你好',
expectedDigest: 'absent',
executable: false
})
expect(request.payloadDigest).toBe(
'sha256:0cf6995c6aa406a5bb1b6a0d2e15955ca3b89ddec26a20f47eaacef02339bea6'
)
const reordered = await createRemoteWorkspaceWriteTextAtomicRequest({
content: '你好',
relativePath: 'src/a.ts',
generation: 1,
workspaceId: 'workspace-1',
executable: false,
expectedDigest: 'absent',
workspaceIdentity: 'identity-1',
operationId: 'operation-1'
})
expect(reordered).toEqual(request)
expect(request).not.toHaveProperty('controllerId')
await expect(
assertRemoteWorkspaceWriteTextAtomicDigest(
{ ...request, content: 'changed after digest' },
'identity-1'
)
).rejects.toThrow(/digest/iu)
})
it('enforces idempotent write terminal metadata and bounded unique change sets', () => {
expect(
remoteWorkspaceWriteResultSchema.parse({
operationId: 'operation-1',
payloadDigest: digest,
status: 'completed',
relativePath: 'src/a.ts',
previousDigest: 'absent',
resultDigest: digest,
byteLength: 3
}).status
).toBe('completed')
expect(() =>
remoteWorkspaceWriteResultSchema.parse({
operationId: 'operation-1',
payloadDigest: digest,
status: 'completed',
relativePath: 'src/a.ts'
})
).toThrow(/metadata/iu)
expect(() =>
remoteWorkspaceApplyChangeSetRequestSchema.parse({
operationId: 'operation-1',
payloadDigest: digest,
workspaceId: 'workspace-1',
generation: 1,
workspaceIdentity: 'identity-1',
changes: [
{
relativePath: 'a.ts',
action: 'delete',
expectedDigest: digest
},
{
relativePath: 'a.ts',
action: 'delete',
expectedDigest: digest
}
]
})
).toThrow(/unique/iu)
})
})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
import { z } from 'zod'
import { componentVersionSchema } from './agent-installation-contracts'
import {
agentIdentifierSchema,
sha256DigestSchema
} from './agent-protocol/contracts'
export const installationRegistryIdSchema = z
.string()
.min(1)
.max(128)
.regex(/^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,126}[A-Za-z0-9])?$/u)
export const installationRegistryEntrySchema = z
.object({
installationId: installationRegistryIdSchema,
agentVersion: componentVersionSchema,
manifestSha256: z.string().regex(/^[a-f0-9]{64}$/u),
arch: z.enum(['x64', 'arm64'])
})
.strict()
export type InstallationRegistryEntry = z.infer<
typeof installationRegistryEntrySchema
>
export const installationRegistryStateSchema = z
.object({
formatVersion: z.literal(1),
current: installationRegistryEntrySchema.optional(),
candidate: installationRegistryEntrySchema.optional()
})
.strict()
.superRefine((state, context) => {
const entries = [
state.current,
state.candidate
].filter((entry): entry is InstallationRegistryEntry =>
entry !== undefined
)
const ids = new Map<string, InstallationRegistryEntry>()
for (const entry of entries) {
const existing = ids.get(entry.installationId)
if (
existing !== undefined &&
!installationEntryEquals(existing, entry)
) {
context.addIssue({
code: 'custom',
path: ['candidate'],
message: 'Agent installation identity is inconsistent across roles'
})
}
ids.set(entry.installationId, entry)
}
if (
state.current !== undefined &&
state.candidate !== undefined &&
state.current.installationId === state.candidate.installationId
) {
context.addIssue({
code: 'custom',
path: ['candidate'],
message: 'Current Agent installation cannot also be the candidate'
})
}
})
export type InstallationRegistryState = z.infer<
typeof installationRegistryStateSchema
>
const compatibleInstallationRegistryStateSchema = z
.object({
formatVersion: z.literal(1),
current: installationRegistryEntrySchema
.passthrough()
.optional(),
candidate: installationRegistryEntrySchema
.passthrough()
.optional()
})
.passthrough()
.superRefine((state, context) => {
const legacyEntry = [state.current, state.candidate].some(
(entry) =>
entry !== undefined &&
(
'releaseSequence' in entry ||
'productVersion' in entry ||
'binaryDigest' in entry
)
)
if (
!legacyEntry &&
!('minimumTrustedReleaseSequence' in state) &&
!('draining' in state) &&
!('rollback' in state)
) {
context.addIssue({
code: 'custom',
message: 'Agent registry is not a recognized legacy format'
})
}
})
export function parseInstallationRegistryState(
value: unknown
): InstallationRegistryState {
const current = installationRegistryStateSchema.safeParse(value)
if (current.success) {
return current.data
}
const compatible =
compatibleInstallationRegistryStateSchema.parse(value)
return installationRegistryStateSchema.parse({
formatVersion: 1,
...(compatible.current === undefined
? {}
: {
current: canonicalInstallationEntry(compatible.current)
}),
...(compatible.candidate === undefined
? {}
: {
candidate: canonicalInstallationEntry(
compatible.candidate
)
})
})
}
export const runtimeRegistryEntrySchema = z
.object({
runtimeId: agentIdentifierSchema,
runtimeVersion: componentVersionSchema,
architecture: z.enum(['x64', 'arm64']),
bundleDigest: sha256DigestSchema,
manifestDigest: sha256DigestSchema,
acpCapabilitiesDigest: sha256DigestSchema
})
.strict()
export type RuntimeRegistryEntry = z.infer<
typeof runtimeRegistryEntrySchema
>
export const runtimeRegistryStateSchema = z
.object({
formatVersion: z.literal(1),
current: z.array(runtimeRegistryEntrySchema).max(16)
})
.strict()
.superRefine((state, context) => {
const identities = new Set<string>()
state.current.forEach((entry, index) => {
const identity = `${entry.runtimeId}:${entry.architecture}`
if (identities.has(identity)) {
context.addIssue({
code: 'custom',
path: ['current', index],
message: 'Runtime registry identities must be unique'
})
}
identities.add(identity)
})
})
export type RuntimeRegistryState = z.infer<
typeof runtimeRegistryStateSchema
>
const compatibleRuntimeRegistryStateSchema = z
.object({
formatVersion: z.literal(1),
current: z
.array(runtimeRegistryEntrySchema.passthrough())
.max(16)
})
.passthrough()
.superRefine((state, context) => {
const legacyEntry = state.current.some(
(entry) =>
'releaseSequence' in entry ||
'signingKeyId' in entry ||
'provider' in entry
)
if (
!legacyEntry &&
!('minimumTrustedReleaseSequence' in state)
) {
context.addIssue({
code: 'custom',
message: 'Runtime registry is not a recognized legacy format'
})
}
})
export function parseRuntimeRegistryState(
value: unknown
): RuntimeRegistryState {
const current = runtimeRegistryStateSchema.safeParse(value)
if (current.success) {
return current.data
}
const compatible =
compatibleRuntimeRegistryStateSchema.parse(value)
return runtimeRegistryStateSchema.parse({
formatVersion: 1,
current: compatible.current.map(canonicalRuntimeEntry)
})
}
function canonicalInstallationEntry(
entry: z.infer<typeof installationRegistryEntrySchema> &
Record<string, unknown>
): InstallationRegistryEntry {
return installationRegistryEntrySchema.parse({
installationId: entry.installationId,
agentVersion: entry.agentVersion,
manifestSha256: entry.manifestSha256,
arch: entry.arch
})
}
function canonicalRuntimeEntry(
entry: z.infer<typeof runtimeRegistryEntrySchema> &
Record<string, unknown>
): RuntimeRegistryEntry {
return runtimeRegistryEntrySchema.parse({
runtimeId: entry.runtimeId,
runtimeVersion: entry.runtimeVersion,
architecture: entry.architecture,
bundleDigest: entry.bundleDigest,
manifestDigest: entry.manifestDigest,
acpCapabilitiesDigest: entry.acpCapabilitiesDigest
})
}
function installationEntryEquals(
left: InstallationRegistryEntry,
right: InstallationRegistryEntry
): boolean {
return (
left.installationId === right.installationId &&
left.agentVersion === right.agentVersion &&
left.manifestSha256 === right.manifestSha256 &&
left.arch === right.arch
)
}
@@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest'
import {
REMOTE_MODEL_GATEWAY_LIMITS,
remoteModelGatewayRequestSchema,
remoteModelGatewayResponseSchema
} from './remote-model-gateway-contracts'
const validRequest = {
method: 'POST',
path: '/v1/messages',
headers: {
accept: 'application/json',
'content-type': 'application/json'
},
bodyBase64: Buffer.from('{"messages":[]}').toString('base64')
} as const
describe('remote model gateway contracts', () => {
it('accepts only POST requests to fixed provider API paths', () => {
expect(remoteModelGatewayRequestSchema.parse(validRequest)).toEqual(
validRequest
)
for (const path of [
'https://api.example/v1/messages',
'//api.example/v1/messages',
'/v1/messages?key=secret',
'/v1/messages#fragment',
'/v1/../messages',
'/images/generations'
]) {
expect(() =>
remoteModelGatewayRequestSchema.parse({ ...validRequest, path })
).toThrow()
}
expect(() =>
remoteModelGatewayRequestSchema.parse({
...validRequest,
method: 'GET'
})
).toThrow()
})
it('rejects credentials and arbitrary headers', () => {
for (const headers of [
{ authorization: 'Bearer remote-secret' },
{ 'x-api-key': 'remote-secret' },
{ cookie: 'private=value' },
{ Accept: 'application/json' },
{ accept: 'ok\r\nx-injected: true' }
]) {
expect(() =>
remoteModelGatewayRequestSchema.parse({ ...validRequest, headers })
).toThrow()
}
})
it('enforces decoded byte limits and canonical base64', () => {
expect(() =>
remoteModelGatewayRequestSchema.parse({
...validRequest,
bodyBase64: Buffer.alloc(
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes + 1
).toString('base64')
})
).toThrow()
expect(() =>
remoteModelGatewayRequestSchema.parse({
...validRequest,
bodyBase64: 'not base64'
})
).toThrow()
expect(() =>
remoteModelGatewayResponseSchema.parse({
status: 200,
headers: { authorization: 'Bearer secret' },
bodyBase64: ''
})
).toThrow()
})
})
@@ -0,0 +1,105 @@
import { z } from 'zod'
import { utf8StringSchema } from './agent-protocol/contracts'
export const REMOTE_MODEL_GATEWAY_LIMITS = {
maximumRequestBodyBytes: 768 * 1024,
maximumResponseBodyBytes: 768 * 1024,
maximumHeaderValueBytes: 1_024
} as const
const requestHeaderNameSchema = z.enum([
'accept',
'content-type'
])
const requestHeaderValueSchema = utf8StringSchema(
REMOTE_MODEL_GATEWAY_LIMITS.maximumHeaderValueBytes,
{
minimumBytes: 1,
label: 'Remote model request header value'
}
).refine(
(value) => !/[\r\n]/u.test(value),
'Header values cannot contain line breaks'
)
const responseHeaderNameSchema = z.enum([
'content-type',
'openai-request-id',
'request-id',
'x-request-id'
])
function boundedCanonicalBase64Schema(maximumBytes: number) {
return z
.string()
.max(Math.ceil(maximumBytes / 3) * 4)
.regex(
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u,
'Body must use canonical base64'
)
.refine(
(value) => Buffer.byteLength(value, 'base64') <= maximumBytes,
`Decoded body exceeds ${maximumBytes} bytes`
)
.refine(
(value) =>
Buffer.from(value, 'base64').toString('base64') === value,
'Body must use canonical base64'
)
}
export const remoteModelApiPathSchema = z.enum([
'/v1/messages',
'/chat/completions',
'/v1/chat/completions',
'/responses',
'/v1/responses'
])
export type RemoteModelApiPath = z.infer<typeof remoteModelApiPathSchema>
export const remoteModelRequestHeadersSchema = z
.partialRecord(requestHeaderNameSchema, requestHeaderValueSchema)
.superRefine((headers, context) => {
for (const name of Object.keys(headers)) {
if (name !== name.toLowerCase()) {
context.addIssue({
code: 'custom',
path: [name],
message: 'Header names must be lowercase'
})
}
}
})
export const remoteModelGatewayRequestSchema = z
.object({
method: z.literal('POST'),
path: remoteModelApiPathSchema,
headers: remoteModelRequestHeadersSchema,
bodyBase64: boundedCanonicalBase64Schema(
REMOTE_MODEL_GATEWAY_LIMITS.maximumRequestBodyBytes
)
})
.strict()
export type RemoteModelGatewayRequest = z.infer<
typeof remoteModelGatewayRequestSchema
>
export const remoteModelResponseHeadersSchema = z.partialRecord(
responseHeaderNameSchema,
requestHeaderValueSchema
)
export const remoteModelGatewayResponseSchema = z
.object({
status: z.number().int().min(100).max(599),
headers: remoteModelResponseHeadersSchema,
bodyBase64: boundedCanonicalBase64Schema(
REMOTE_MODEL_GATEWAY_LIMITS.maximumResponseBodyBytes
)
})
.strict()
export type RemoteModelGatewayResponse = z.infer<
typeof remoteModelGatewayResponseSchema
>
@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest'
import {
REMOTE_PROJECT_SAVE_LIMITS,
remoteProjectCreateDraftSchema,
remoteProjectSaveProgressSchema,
remoteProjectSaveRequestSchema,
remoteProjectUpdateDraftSchema
} from './remote-project-candidate-contracts'
const hostId = '00000000-0000-4000-8000-000000000010'
const projectId = '00000000-0000-4000-8000-000000000020'
const profileId = '00000000-0000-4000-8000-000000000040'
const draft = {
name: '远程项目',
description: 'Awaited save',
defaultWorkMode: 'ask' as const,
runtimeSelection: { provider: 'model' as const, profileId },
hostId,
remoteRootPath: '/srv/projects/goodbuddy'
}
describe('remote project save contracts', () => {
it('accepts closed create and update save requests', () => {
expect(
remoteProjectSaveRequestSchema.parse({
intent: 'create',
draft
})
).toEqual({ intent: 'create', draft })
expect(
remoteProjectSaveRequestSchema.parse({
intent: 'update',
draft: { projectId, ...draft }
})
).toEqual({ intent: 'update', draft: { projectId, ...draft } })
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
candidateId: projectId
})
).toThrow()
expect(() =>
remoteProjectUpdateDraftSchema.parse({
projectId,
...draft,
commitToken: 'token'
})
).toThrow()
})
it('accepts Execute without consent or trust fields', () => {
expect(
remoteProjectCreateDraftSchema.parse({
...draft,
defaultWorkMode: 'execute'
}).defaultWorkMode
).toBe('execute')
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
trust: {},
defaultWorkMode: 'execute'
})
).toThrow()
})
it('requires a Runtime and a bounded absolute remote root', () => {
const withoutRuntime: Partial<typeof draft> = { ...draft }
delete withoutRuntime.runtimeSelection
expect(() =>
remoteProjectCreateDraftSchema.parse(withoutRuntime)
).toThrow()
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
remoteRootPath: 'srv/project'
})
).toThrow()
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
remoteRootPath: '/srv/\nprivate'
})
).toThrow(/control characters/iu)
for (const remoteRootPath of [
'/srv//project',
'/srv/./project',
'/srv/../project'
]) {
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
remoteRootPath
})
).toThrow(/empty, dot, or parent segments/iu)
}
expect(() =>
remoteProjectCreateDraftSchema.parse({
...draft,
remoteRootPath: `/${'界'.repeat(
Math.floor(
REMOTE_PROJECT_SAVE_LIMITS.maximumAbsolutePathBytes / 3
) + 1
)}`
})
).toThrow(/4096 UTF-8 bytes/iu)
})
it('exposes only an ephemeral progress phase', () => {
expect(remoteProjectSaveProgressSchema.parse({ phase: 'agent' })).toEqual({
phase: 'agent'
})
expect(() =>
remoteProjectSaveProgressSchema.parse({
phase: 'agent',
candidateId: projectId
})
).toThrow()
})
})
@@ -0,0 +1,94 @@
import { z } from 'zod'
import { assistantIdSchema, projectCreateSchema } from './assistant-contracts'
import {
REMOTE_WORKSPACE_LIMITS,
remoteAbsolutePathSchema
} from './remote-agent-contracts'
import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
import { sshHostIdSchema } from './ssh-host-contracts'
export const REMOTE_PROJECT_SAVE_LIMITS = {
maximumAbsolutePathBytes:
REMOTE_WORKSPACE_LIMITS.maximumAbsolutePathBytes
} as const
export const remoteProjectRootPathSchema = remoteAbsolutePathSchema
.refine(
(value) => !/[\p{Cc}\p{Cs}\u2028\u2029\ufffd]/u.test(value),
'Remote project root cannot contain control characters'
)
.refine(
(value) =>
!value.includes('//') &&
!value
.split('/')
.some((segment) => segment === '.' || segment === '..'),
'Remote project root cannot contain empty, dot, or parent segments'
)
const commonDraftFields = {
name: projectCreateSchema.shape.name,
description: projectCreateSchema.shape.description,
defaultWorkMode: projectCreateSchema.shape.defaultWorkMode,
runtimeSelection: agentRuntimeSelectionSchema,
hostId: sshHostIdSchema,
remoteRootPath: remoteProjectRootPathSchema
} as const
export const remoteProjectCreateDraftSchema =
z.object(commonDraftFields).strict()
export const remoteProjectUpdateDraftSchema = z
.object({
projectId: assistantIdSchema,
...commonDraftFields
})
.strict()
export const remoteProjectSaveRequestSchema = z.discriminatedUnion(
'intent',
[
z
.object({
intent: z.literal('create'),
draft: remoteProjectCreateDraftSchema
})
.strict(),
z
.object({
intent: z.literal('update'),
draft: remoteProjectUpdateDraftSchema
})
.strict()
]
)
export const remoteProjectSavePhaseSchema = z.enum([
'host',
'agent',
'workspace',
'runtime',
'saving'
])
export const remoteProjectSaveProgressSchema = z
.object({
phase: remoteProjectSavePhaseSchema
})
.strict()
export type RemoteProjectCreateDraft = z.infer<
typeof remoteProjectCreateDraftSchema
>
export type RemoteProjectUpdateDraft = z.infer<
typeof remoteProjectUpdateDraftSchema
>
export type RemoteProjectSaveRequest = z.infer<
typeof remoteProjectSaveRequestSchema
>
export type RemoteProjectSavePhase = z.infer<
typeof remoteProjectSavePhaseSchema
>
export type RemoteProjectSaveProgress = z.infer<
typeof remoteProjectSaveProgressSchema
>
@@ -0,0 +1,548 @@
import { describe, expect, it } from 'vitest'
import { createOperationIdentity } from './agent-protocol/canonical'
import {
REMOTE_RUNTIME_LAUNCH_LIMITS,
assertDetachedRemoteRuntimeBundleDigest,
assertRemoteRuntimeOperationDigest,
createRemoteRuntimeQueryRequest,
createRemoteRuntimeStartRequest,
createRemoteRuntimeStopRequest,
detachedSignedRemoteRuntimeBundleSchema,
digestRemoteRuntimeBundleIdentity,
digestRemoteRuntimeBundleManifest,
remoteRuntimeQueryRequestSchema,
remoteRuntimeQueryResultSchema,
remoteRuntimeStartRequestSchema,
remoteRuntimeStartResultSchema,
remoteRuntimeStopRequestSchema,
remoteRuntimeStopResultSchema
} from './remote-runtime-launch-contracts'
const digest = `sha256:${'a'.repeat(64)}`
const rawDigest = 'a'.repeat(64)
const deadlineAt = '2030-01-01T00:00:00.000Z'
const manifest = {
formatVersion: 2 as const,
product: 'GoodBuddy' as const,
runtimeId: 'runtime-1',
runtimeVersion: '1.0.0',
provider: 'opencode' as const,
platform: 'linux' as const,
architecture: 'x64' as const,
signingKeyId: 'release-1',
bundleDigest: digest,
adapterDigest: digest,
sourcePackage: {
name: 'opencode-linux-x64-baseline',
integrity:
'sha512-x4KiJk9EF7ktM18Ru5Jue4kTntxMvlhWb7tHniQGGRvY2KeoK1iIkyAFd7ri5H/fSkM22hNv/Gg1Jk6/h9IlxQ=='
},
entrypoint: {
identity: 'opencode-acp',
path: 'bin/opencode-acp',
sha256: rawDigest,
argvPrefix: ['acp']
},
files: [
{
path: 'bin/opencode-acp',
size: 100,
sha256: rawDigest,
mode: '0755' as const
},
{
path: 'licenses/runtime.txt',
size: 10,
sha256: rawDigest,
mode: '0644' as const
}
],
licenses: [
{
package: 'runtime',
version: '1.0.0',
spdx: 'MIT',
path: 'licenses/runtime.txt'
}
],
allowedEnvironmentNames: ['HOME', 'LANG'] as const,
protocol: { major: 1, minor: 0 },
acpCapabilitiesDigest: digest,
limits: {
maximumPromptRuntimeMilliseconds: 60_000,
maximumPromptInputBytes: 4096,
maximumPromptOutputBytes: 1024
}
}
const signedBundle = {
manifest,
detachedSignature: {
manifestDigest: digest,
algorithm: 'ed25519' as const,
keyId: 'release-1',
signatureBase64: `${'A'.repeat(86)}==`
}
}
const commonPreparation = {
bindingId: 'binding-1',
operationId: 'operation-1',
requestId: 'request-1',
workMode: 'ask' as const,
controllerId: 'controller-1',
controllerGeneration: 1,
connectionGeneration: 1,
channelEpoch: '1',
hostId: 'host-1',
hostRevision: 1,
hostKeyGeneration: 1,
workspaceIdentity: 'workspace-1',
agentInstallationId: 'installation-1',
runtimeId: 'runtime-1',
runtimeBundleDigest: digest,
runtimeAdapterDigest: digest,
deadlineAt,
budget: {
maximumInputBytes: 10,
maximumOutputBytes: 100
}
}
const startPayload = {
bindingId: 'binding-1',
requestId: 'request-1',
workMode: 'ask' as const,
runtimeId: 'runtime-1',
adapterParameters: [
{ name: 'modelProfileId' as const, value: 'model-1' }
],
deadlineAt,
budget: commonPreparation.budget
}
async function operationRequest<T>(
method: string,
operationId: string,
requestId: string,
payload: T
) {
return {
identity: await createOperationIdentity({
controllerId: 'controller-1',
operationId,
scope: {
kind: 'run',
sessionId: 'binding-1',
requestId
},
method,
payload
}),
payload
}
}
const forbiddenLaunchFields = [
{ executable: '/bin/sh' },
{ cwd: '/tmp' },
{ env: { TOKEN: 'secret' } },
{ unit: 'runtime.service' },
{ systemdProperties: ['Delegate=yes'] },
{ network: 'host' },
{ mount: '/etc:/host' },
{ mounts: ['/etc:/host'] },
{ shell: true },
{ pid: 123 },
{ signal: 'SIGKILL' }
]
describe('remote Runtime launch contracts', () => {
it.each(['deepseek-harness', 'opencode', 'continue'] as const)(
'accepts a detached-signed %s bundle without signature metadata in signed bytes',
(provider) => {
const parsed = detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: { ...manifest, provider }
})
expect(parsed.manifest.provider).toBe(provider)
expect(parsed.manifest).not.toHaveProperty('manifestDigest')
expect(parsed.manifest).not.toHaveProperty('signature')
}
)
it('binds detached keys, entrypoint files, licenses, and unique environment names', () => {
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
detachedSignature: {
...signedBundle.detachedSignature,
keyId: 'other-key'
}
})
).toThrow(/key/iu)
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: {
...manifest,
entrypoint: { ...manifest.entrypoint, sha256: 'b'.repeat(64) }
}
})
).toThrow(/entrypoint/iu)
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: {
...manifest,
licenses: [
{ ...manifest.licenses[0], path: 'licenses/missing.txt' }
]
}
})
).toThrow(/licenses/iu)
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: {
...manifest,
allowedEnvironmentNames: ['HOME', 'HOME']
}
})
).toThrow(/unique/iu)
})
it('bounds prompt limits and fixed argv', () => {
expect(
detachedSignedRemoteRuntimeBundleSchema.parse(signedBundle)
).toEqual(signedBundle)
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: {
...manifest,
limits: {
...manifest.limits,
maximumPromptInputBytes:
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumPromptInputBytes + 1
}
}
})
).toThrow()
expect(() =>
detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest: {
...manifest,
entrypoint: {
...manifest.entrypoint,
argvPrefix: Array(
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumFixedArgumentCount + 1
).fill('x')
}
}
})
).toThrow()
})
it('keeps one current prompt-limits manifest contract', () => {
const parsed = detachedSignedRemoteRuntimeBundleSchema.parse({
...signedBundle,
manifest
})
expect(parsed.manifest.formatVersion).toBe(2)
expect(parsed.manifest).toHaveProperty('limits')
expect(Object.keys(parsed.manifest)).not.toContain('quotas')
})
it('verifies the detached digest over manifest bytes only', async () => {
const bundleDigest =
await digestRemoteRuntimeBundleIdentity(manifest)
const digestBoundManifest = { ...manifest, bundleDigest }
const manifestDigest =
await digestRemoteRuntimeBundleManifest(digestBoundManifest)
const validBundle = {
...signedBundle,
manifest: digestBoundManifest,
detachedSignature: {
...signedBundle.detachedSignature,
manifestDigest
}
}
await expect(
assertDetachedRemoteRuntimeBundleDigest(validBundle)
).resolves.toEqual(validBundle)
await expect(
assertDetachedRemoteRuntimeBundleDigest(signedBundle)
).rejects.toThrow(/digest does not match/iu)
})
it('constructs the exact start operation identity and canonical digest', async () => {
const request = await createRemoteRuntimeStartRequest({
controllerId: 'controller-1',
operationId: 'start-operation-1',
prompt: commonPreparation,
adapterParameters: startPayload.adapterParameters,
deadlineAt
})
expect(request.identity.method).toBe('runtime/startPrompt')
expect(request.identity.scope).toEqual({
kind: 'run',
sessionId: 'binding-1',
requestId: 'request-1'
})
const expected = await createOperationIdentity({
controllerId: 'controller-1',
operationId: 'start-operation-1',
scope: request.identity.scope,
method: 'runtime/startPrompt',
payload: request.payload
})
expect(request.identity.payloadDigest).toBe(expected.payloadDigest)
expect(request.payload.workMode).toBe('ask')
await expect(
createRemoteRuntimeStartRequest({
controllerId: 'controller-1',
operationId: commonPreparation.operationId,
prompt: commonPreparation,
deadlineAt: '2031-01-01T00:00:00.000Z'
})
).rejects.toThrow(/prompt deadline/iu)
})
it('rejects caller digest mismatches', async () => {
const valid = await operationRequest(
'runtime/startPrompt',
'start-operation-1',
'request-1',
startPayload
)
const changedPayload = {
...startPayload,
runtimeId: 'runtime-2'
}
await expect(
assertRemoteRuntimeOperationDigest({
...valid,
payload: changedPayload
})
).rejects.toThrow(/digest does not match/iu)
await expect(
assertRemoteRuntimeOperationDigest(valid)
).resolves.toBeUndefined()
await expect(
assertRemoteRuntimeOperationDigest({
...valid,
identity: { ...valid.identity, payloadDigest: digest }
})
).rejects.toThrow(/digest does not match/iu)
})
it('rejects adapter flag and path tunneling plus all process-control fields', async () => {
for (const value of [
'--flag',
'/tmp/model',
'models/model',
'models\\model',
'..'
]) {
await expect(
createRemoteRuntimeStartRequest({
controllerId: 'controller-1',
operationId: 'start-operation-1',
prompt: commonPreparation,
adapterParameters: [
{ name: 'modelProfileId', value }
],
deadlineAt
})
).rejects.toThrow()
}
const valid = await operationRequest(
'runtime/startPrompt',
'start-operation-1',
'request-1',
startPayload
)
for (const forbidden of forbiddenLaunchFields) {
expect(() =>
remoteRuntimeStartRequestSchema.parse({
...valid,
payload: { ...startPayload, ...forbidden }
})
).toThrow()
}
})
it('operation-binds start results and canonical stop/query requests', async () => {
const startResult = {
launchId: 'launch-1',
processId: 'process-opaque-1',
startOperationId: 'start-operation-1',
bindingId: 'binding-1',
runtimeId: 'runtime-1',
supervisorIdentityDigest: digest,
state: 'running' as const,
acceptedAt: deadlineAt
}
expect(remoteRuntimeStartResultSchema.parse(startResult)).toEqual(
startResult
)
const stopPayload = {
launchId: 'launch-1',
bindingId: 'binding-1',
runtimeId: 'runtime-1',
startOperationId: 'start-operation-1',
requestId: 'stop-request-1',
reason: 'user-cancelled' as const,
deadlineAt
}
const stop = await createRemoteRuntimeStopRequest({
controllerId: 'controller-1',
operationId: 'stop-1',
payload: stopPayload
})
expect(remoteRuntimeStopRequestSchema.parse(stop)).toEqual(stop)
const query = await createRemoteRuntimeQueryRequest({
controllerId: 'controller-1',
operationId: 'query-1',
payload: {
launchId: 'launch-1',
bindingId: 'binding-1',
runtimeId: 'runtime-1',
startOperationId: 'start-operation-1',
requestId: 'query-request-1'
}
})
expect(remoteRuntimeQueryRequestSchema.parse(query)).toEqual(query)
expect(() =>
remoteRuntimeStopRequestSchema.parse({
...stop,
identity: { ...stop.identity, method: 'runtime/startPrompt' }
})
).toThrow(/method/iu)
expect(() =>
remoteRuntimeQueryRequestSchema.parse({
...query,
payload: { ...query.payload, bindingId: 'other-binding' }
})
).toThrow(/scope/iu)
for (const forbidden of forbiddenLaunchFields) {
expect(() =>
remoteRuntimeStopRequestSchema.parse({
...stop,
payload: { ...stop.payload, ...forbidden }
})
).toThrow()
expect(() =>
remoteRuntimeQueryRequestSchema.parse({
...query,
payload: { ...query.payload, ...forbidden }
})
).toThrow()
}
})
it('requires ownership identity in stop results and finite reconciliation state', () => {
const stopResult = {
launchId: 'launch-1',
operationId: 'stop-1',
startOperationId: 'start-operation-1',
bindingId: 'binding-1',
runtimeId: 'runtime-1',
supervisorIdentityDigest: digest,
state: 'stopped' as const,
acceptedAt: deadlineAt
}
expect(remoteRuntimeStopResultSchema.parse(stopResult)).toEqual(
stopResult
)
const baseQueryResult = {
launchId: 'launch-1',
queryOperationId: 'query-1',
startOperationId: 'start-operation-1',
bindingId: 'binding-1',
runtimeId: 'runtime-1',
supervisorIdentityDigest: digest,
output: {
effectiveCapturedOutputLimitBytes: 1024,
stdoutBytes: 10,
stderrBytes: 2,
truncated: false
}
}
expect(
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'interrupted',
processTreeState: 'orphaned',
errorCode: 'process-tree-orphaned'
}).state
).toBe('interrupted')
expect(
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'outcome-unknown',
processTreeState: 'outcome-unknown',
errorCode: 'supervisor-unavailable'
}).state
).toBe('outcome-unknown')
expect(() =>
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'identity-conflict',
processTreeState: 'reconciled',
errorCode: 'identity-mismatch'
})
).toThrow(/inconsistent/iu)
for (const invalid of [
{ state: 'running', processTreeState: 'terminated' },
{ state: 'stopped', processTreeState: 'pending' },
{ state: 'stopped', processTreeState: 'orphaned' }
] as const) {
expect(() =>
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
...invalid
})
).toThrow(/inconsistent/iu)
}
expect(() =>
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'running',
processTreeState: 'reconciled',
output: {
effectiveCapturedOutputLimitBytes: 10,
stdoutBytes: 8,
stderrBytes: 3,
truncated: true
}
})
).toThrow(/aggregate limit/iu)
expect(() =>
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'running',
processTreeState: 'reconciled',
output: {
effectiveCapturedOutputLimitBytes:
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes,
stdoutBytes:
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes,
stderrBytes: 1,
truncated: true
}
})
).toThrow(/aggregate limit/iu)
expect(() =>
remoteRuntimeQueryResultSchema.parse({
...baseQueryResult,
state: 'running',
processTreeState: 'reconciled',
diagnostic: 'arbitrary process text'
})
).toThrow()
})
})
@@ -0,0 +1,793 @@
import { z } from 'zod'
import {
agentArchitectureSchema,
agentBundleManifestSchema,
agentManifestLicenseSchema,
agentManifestPathSchema,
agentProtocolVersionSchema,
agentReleaseKeySchema
} from './agent-installation-contracts'
import {
agentIdentifierSchema,
operationIdentitySchema,
sha256DigestSchema,
utf8StringSchema,
type OperationIdentity
} from './agent-protocol/contracts'
import {
createOperationIdentity,
digestCanonicalOperation
} from './agent-protocol/canonical'
import {
remotePromptOperationPreparationSchema,
type RemotePromptOperationPreparation
} from './remote-agent-contracts'
export const REMOTE_RUNTIME_LAUNCH_LIMITS = {
maximumFiles: 10_000,
maximumLicenses: 1_000,
maximumFixedArgumentCount: 32,
maximumFixedArgumentBytes: 1024,
maximumAdapterParameters: 8,
maximumEnvironmentNames: 16,
maximumRuntimeMilliseconds: 24 * 60 * 60 * 1000,
maximumPromptInputBytes: 16 * 1024 * 1024,
maximumCapturedOutputBytes: 16 * 1024 * 1024
} as const
export const remoteRuntimeProviderKindSchema = z.enum([
'deepseek-harness',
'opencode',
'continue'
])
export const remoteRuntimeArchitectureSchema = agentArchitectureSchema
export const remoteRuntimeAllowedEnvironmentNameSchema = z.enum([
'HOME',
'LANG',
'LC_ALL',
'PATH',
'TMPDIR',
'XDG_CACHE_HOME',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_STATE_HOME'
])
export const OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES = [
'HOME',
'LANG',
'LC_ALL',
'PATH',
'TMPDIR',
'XDG_CACHE_HOME',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_STATE_HOME'
] as const
const fixedArgumentSchema = utf8StringSchema(
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumFixedArgumentBytes,
{ label: 'Fixed Runtime argument' }
).refine((value) => !value.includes('\0'), 'Runtime argument contains NUL')
const rawSha256Schema = z.string().regex(/^[a-f0-9]{64}$/u)
const npmPackageIntegritySchema = z
.string()
.regex(/^sha512-[A-Za-z0-9+/]{86}==$/u)
export const remoteRuntimeSourcePackageSchema = z
.object({
name: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,127}$/u),
integrity: npmPackageIntegritySchema
})
.strict()
export const remoteRuntimeBundleFileSchema = z
.object({
path: agentManifestPathSchema,
size: z.number().int().min(0).safe(),
sha256: rawSha256Schema,
mode: z.enum(['0644', '0755'])
})
.strict()
export const remoteRuntimeLimitsSchema = z
.object({
maximumPromptRuntimeMilliseconds: z
.number()
.int()
.min(1)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumRuntimeMilliseconds),
maximumPromptInputBytes: z
.number()
.int()
.min(1)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumPromptInputBytes),
maximumPromptOutputBytes: z
.number()
.int()
.min(0)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes)
})
.strict()
const remoteRuntimeManifestFields = {
product: z.literal('GoodBuddy'),
runtimeId: agentIdentifierSchema,
runtimeVersion: agentBundleManifestSchema.shape.agentVersion,
provider: remoteRuntimeProviderKindSchema,
platform: z.literal('linux'),
architecture: agentArchitectureSchema,
signingKeyId: agentReleaseKeySchema.shape.keyId,
bundleDigest: sha256DigestSchema,
adapterDigest: sha256DigestSchema,
sourcePackage: remoteRuntimeSourcePackageSchema,
entrypoint: z
.object({
identity: agentIdentifierSchema,
path: agentManifestPathSchema,
sha256: rawSha256Schema,
argvPrefix: z
.array(fixedArgumentSchema)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumFixedArgumentCount)
})
.strict(),
files: z
.array(remoteRuntimeBundleFileSchema)
.min(1)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumFiles),
licenses: z
.array(agentManifestLicenseSchema)
.min(1)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumLicenses),
allowedEnvironmentNames: z
.array(remoteRuntimeAllowedEnvironmentNameSchema)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumEnvironmentNames),
protocol: agentProtocolVersionSchema,
acpCapabilitiesDigest: sha256DigestSchema
} as const
export const remoteRuntimeBundleManifestSchema = z
.object({
formatVersion: z.literal(2),
...remoteRuntimeManifestFields,
limits: remoteRuntimeLimitsSchema
})
.strict()
.superRefine((manifest, context) => {
const files = new Map<string, string>()
manifest.files.forEach((file, index) => {
if (
file.path === 'manifest.json' ||
file.path === 'manifest.sig'
) {
context.addIssue({
code: 'custom',
path: ['files', index, 'path'],
message: 'Runtime payload cannot declare signature metadata'
})
}
if (files.has(file.path)) {
context.addIssue({
code: 'custom',
path: ['files', index, 'path'],
message: 'Runtime bundle file paths must be unique'
})
}
files.set(file.path, file.sha256)
})
if (files.get(manifest.entrypoint.path) !== manifest.entrypoint.sha256) {
context.addIssue({
code: 'custom',
path: ['entrypoint'],
message: 'Runtime entrypoint must match a declared bundle file'
})
}
const licensePaths = new Set<string>()
manifest.licenses.forEach((license, index) => {
if (licensePaths.has(license.path) || !files.has(license.path)) {
context.addIssue({
code: 'custom',
path: ['licenses', index, 'path'],
message: 'Runtime licenses must uniquely name declared files'
})
}
licensePaths.add(license.path)
})
if (
new Set(manifest.allowedEnvironmentNames).size !==
manifest.allowedEnvironmentNames.length
) {
context.addIssue({
code: 'custom',
path: ['allowedEnvironmentNames'],
message: 'Allowed environment names must be unique'
})
}
})
export type RemoteRuntimeBundleManifest = z.infer<
typeof remoteRuntimeBundleManifestSchema
>
const remoteRuntimeLockedTargetSchema = z
.object({
package: remoteRuntimeSourcePackageSchema.shape.name,
integrity: npmPackageIntegritySchema
})
.strict()
export const remoteRuntimeLockSchema = z
.object({
formatVersion: z.literal(1),
runtimes: z
.object({
opencode: z
.object({
version: z.literal('1.18.9'),
provider: z.literal('opencode'),
entrypoint: z.literal('bin/opencode'),
entrypointIdentity: z.literal('opencode-acp'),
argvPrefix: z.tuple([z.literal('acp')]),
allowedEnvironmentNames: z.tuple([
z.literal('HOME'),
z.literal('LANG'),
z.literal('LC_ALL'),
z.literal('PATH'),
z.literal('TMPDIR'),
z.literal('XDG_CACHE_HOME'),
z.literal('XDG_CONFIG_HOME'),
z.literal('XDG_DATA_HOME'),
z.literal('XDG_STATE_HOME')
]),
protocol: agentProtocolVersionSchema,
targets: z
.object({
x64: remoteRuntimeLockedTargetSchema.extend({
package: z.literal(
'opencode-linux-x64-baseline'
)
}),
arm64: remoteRuntimeLockedTargetSchema.extend({
package: z.literal('opencode-linux-arm64')
})
})
.strict()
})
.strict()
})
.strict()
})
.strict()
export type RemoteRuntimeLock = z.infer<typeof remoteRuntimeLockSchema>
export const remoteRuntimeDetachedSignatureSchema = z
.object({
manifestDigest: sha256DigestSchema,
algorithm: z.literal('ed25519'),
keyId: agentReleaseKeySchema.shape.keyId,
signatureBase64: z
.string()
.regex(/^(?:[A-Za-z0-9+/]{4}){21}[A-Za-z0-9+/]{2}==$/u)
})
.strict()
export const detachedSignedRemoteRuntimeBundleSchema = z
.object({
manifest: remoteRuntimeBundleManifestSchema,
detachedSignature: remoteRuntimeDetachedSignatureSchema
})
.strict()
.superRefine((bundle, context) => {
if (
bundle.manifest.signingKeyId !== bundle.detachedSignature.keyId
) {
context.addIssue({
code: 'custom',
path: ['detachedSignature', 'keyId'],
message: 'Detached signature key does not match the manifest'
})
}
})
export const signedRemoteRuntimeManifestSchema =
detachedSignedRemoteRuntimeBundleSchema
export type SignedRemoteRuntimeManifest = z.infer<
typeof detachedSignedRemoteRuntimeBundleSchema
>
export const remoteRuntimeDeadlineSchema = z
.string()
.datetime({ offset: true })
.refine((value) => Number.isFinite(Date.parse(value)), 'Invalid deadline')
export const remoteRuntimeAdapterParameterSchema = z
.object({
name: z.enum([
'modelProfileId',
'configurationId',
'conversationAdapterId'
]),
value: agentIdentifierSchema.refine(
(value) =>
!value.startsWith('-') &&
!value.includes('/') &&
!value.includes('\\') &&
!value.includes('\0') &&
value !== '.' &&
value !== '..',
'Adapter parameter must be an opaque identifier'
)
})
.strict()
const remoteRuntimeAdapterParametersSchema = z
.array(remoteRuntimeAdapterParameterSchema)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumAdapterParameters)
.superRefine((parameters, context) => {
const names = new Set<string>()
parameters.forEach((parameter, index) => {
if (names.has(parameter.name)) {
context.addIssue({
code: 'custom',
path: [index, 'name'],
message: 'Adapter parameter names must be unique'
})
}
names.add(parameter.name)
})
})
export const remoteRuntimeStartPayloadSchema = z
.object({
bindingId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
workMode: z.enum(['ask', 'execute']),
runtimeId: agentIdentifierSchema,
adapterParameters: remoteRuntimeAdapterParametersSchema,
deadlineAt: remoteRuntimeDeadlineSchema,
budget: remotePromptOperationPreparationSchema.shape.budget
})
.strict()
export type RemoteRuntimeStartPayload = z.infer<
typeof remoteRuntimeStartPayloadSchema
>
export const remoteRuntimeStartRequestSchema = z
.object({
identity: operationIdentitySchema,
payload: remoteRuntimeStartPayloadSchema
})
.strict()
.superRefine((request, context) => {
addOperationBindingIssues(
request.identity,
'runtime/startPrompt',
request.payload.bindingId,
request.payload.requestId,
context
)
})
export type RemoteRuntimeStartRequest = z.infer<
typeof remoteRuntimeStartRequestSchema
>
export const remoteRuntimeStartResultSchema = z
.object({
launchId: agentIdentifierSchema,
processId: agentIdentifierSchema,
startOperationId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
supervisorIdentityDigest: sha256DigestSchema,
state: z.enum(['starting', 'running']),
acceptedAt: z.string().datetime({ offset: true })
})
.strict()
export type RemoteRuntimeStartResult = z.infer<
typeof remoteRuntimeStartResultSchema
>
export const remoteRuntimeStopReasonSchema = z.enum([
'user-cancelled',
'deadline-exceeded',
'binding-closed',
'shutdown',
'identity-conflict'
])
export const remoteRuntimeStopPayloadSchema = z
.object({
launchId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
startOperationId: agentIdentifierSchema,
requestId: agentIdentifierSchema,
reason: remoteRuntimeStopReasonSchema,
deadlineAt: remoteRuntimeDeadlineSchema
})
.strict()
export const remoteRuntimeStopRequestSchema = z
.object({
identity: operationIdentitySchema,
payload: remoteRuntimeStopPayloadSchema
})
.strict()
.superRefine((request, context) => {
addOperationBindingIssues(
request.identity,
'runtime/stopPrompt',
request.payload.bindingId,
request.payload.requestId,
context
)
})
export const remoteRuntimeStopResultSchema = z
.object({
launchId: agentIdentifierSchema,
operationId: agentIdentifierSchema,
startOperationId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
supervisorIdentityDigest: sha256DigestSchema,
state: z.enum(['stop-requested', 'stopped']),
acceptedAt: z.string().datetime({ offset: true })
})
.strict()
export const remoteRuntimeQueryPayloadSchema = z
.object({
launchId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
startOperationId: agentIdentifierSchema,
requestId: agentIdentifierSchema
})
.strict()
export const remoteRuntimeQueryRequestSchema = z
.object({
identity: operationIdentitySchema,
payload: remoteRuntimeQueryPayloadSchema
})
.strict()
.superRefine((request, context) => {
addOperationBindingIssues(
request.identity,
'runtime/queryPrompt',
request.payload.bindingId,
request.payload.requestId,
context
)
})
export const remoteRuntimeLaunchStateSchema = z.enum([
'starting',
'running',
'stop-requested',
'stopped',
'failed',
'expired',
'interrupted',
'outcome-unknown',
'identity-conflict'
])
export const remoteRuntimeProcessTreeStateSchema = z.enum([
'pending',
'reconciled',
'terminating',
'terminated',
'orphaned',
'outcome-unknown',
'identity-conflict'
])
export const REMOTE_RUNTIME_STATE_PROCESS_TREE_MATRIX = {
starting: ['pending', 'reconciled'],
running: ['reconciled'],
'stop-requested': ['reconciled', 'terminating'],
stopped: ['terminated'],
failed: ['pending', 'terminated', 'orphaned'],
expired: ['terminating', 'terminated', 'orphaned'],
interrupted: [
'reconciled',
'terminating',
'terminated',
'orphaned',
'outcome-unknown'
],
'outcome-unknown': ['outcome-unknown'],
'identity-conflict': ['identity-conflict']
} as const satisfies Record<
z.infer<typeof remoteRuntimeLaunchStateSchema>,
readonly z.infer<typeof remoteRuntimeProcessTreeStateSchema>[]
>
export const remoteRuntimeErrorCodeSchema = z.enum([
'manifest-untrusted',
'identity-mismatch',
'deadline-exceeded',
'output-limit-exceeded',
'launch-failed',
'runtime-failed',
'supervisor-unavailable',
'process-tree-orphaned',
'not-found'
])
export const remoteRuntimeOutputMetadataSchema = z
.object({
effectiveCapturedOutputLimitBytes: z
.number()
.int()
.min(0)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes),
stdoutBytes: z
.number()
.int()
.min(0)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes),
stderrBytes: z
.number()
.int()
.min(0)
.max(REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes),
truncated: z.boolean()
})
.strict()
.superRefine((output, context) => {
const aggregate = output.stdoutBytes + output.stderrBytes
if (
aggregate > output.effectiveCapturedOutputLimitBytes ||
aggregate >
REMOTE_RUNTIME_LAUNCH_LIMITS.maximumCapturedOutputBytes
) {
context.addIssue({
code: 'custom',
path: ['effectiveCapturedOutputLimitBytes'],
message: 'Captured Runtime output exceeds its aggregate limit'
})
}
})
export const remoteRuntimeQueryResultSchema = z
.object({
launchId: agentIdentifierSchema,
queryOperationId: agentIdentifierSchema,
startOperationId: agentIdentifierSchema,
bindingId: agentIdentifierSchema,
runtimeId: agentIdentifierSchema,
supervisorIdentityDigest: sha256DigestSchema,
state: remoteRuntimeLaunchStateSchema,
processTreeState: remoteRuntimeProcessTreeStateSchema,
output: remoteRuntimeOutputMetadataSchema,
errorCode: remoteRuntimeErrorCodeSchema.optional()
})
.strict()
.superRefine((result, context) => {
const allowedProcessTreeStates =
REMOTE_RUNTIME_STATE_PROCESS_TREE_MATRIX[result.state]
if (
!(allowedProcessTreeStates as readonly string[]).includes(
result.processTreeState
)
) {
context.addIssue({
code: 'custom',
path: ['processTreeState'],
message:
'Runtime launch and process-tree states are inconsistent'
})
}
const needsError =
result.state === 'failed' ||
result.state === 'interrupted' ||
result.state === 'outcome-unknown' ||
result.state === 'identity-conflict'
if (needsError !== (result.errorCode !== undefined)) {
context.addIssue({
code: 'custom',
path: ['errorCode'],
message: 'Runtime error code does not match the durable state'
})
}
})
export type RemoteRuntimeStopRequest = z.infer<
typeof remoteRuntimeStopRequestSchema
>
export type RemoteRuntimeStopResult = z.infer<
typeof remoteRuntimeStopResultSchema
>
export type RemoteRuntimeQueryRequest = z.infer<
typeof remoteRuntimeQueryRequestSchema
>
export type RemoteRuntimeQueryResult = z.infer<
typeof remoteRuntimeQueryResultSchema
>
export async function digestRemoteRuntimePreparation(
preparation: RemotePromptOperationPreparation
): Promise<string> {
const parsed = remotePromptOperationPreparationSchema.parse(preparation)
return digestCanonicalOperation({
method: 'runtime/preparePrompt',
scope: {
kind: 'run',
sessionId: parsed.bindingId,
requestId: parsed.requestId
},
payload: parsed
})
}
export async function createRemoteRuntimeStartRequest(input: {
controllerId: string
operationId: string
prompt: unknown
adapterParameters?: unknown
deadlineAt?: string
}): Promise<RemoteRuntimeStartRequest> {
const prompt = remotePromptOperationPreparationSchema.parse(input.prompt)
const deadlineAt = input.deadlineAt ?? prompt.deadlineAt
if (Date.parse(deadlineAt) > Date.parse(prompt.deadlineAt)) {
throw new Error('Runtime start deadline exceeds the prompt deadline')
}
const payload = remoteRuntimeStartPayloadSchema.parse({
bindingId: prompt.bindingId,
requestId: prompt.requestId,
workMode: prompt.workMode,
runtimeId: prompt.runtimeId,
adapterParameters: input.adapterParameters ?? [],
deadlineAt,
budget: prompt.budget
})
const identity = await createOperationIdentity({
controllerId: agentIdentifierSchema.parse(input.controllerId),
operationId: agentIdentifierSchema.parse(input.operationId),
scope: {
kind: 'run',
sessionId: prompt.bindingId,
requestId: prompt.requestId
},
method: 'runtime/startPrompt',
payload
})
return remoteRuntimeStartRequestSchema.parse({ identity, payload })
}
export async function createRemoteRuntimeStopRequest(input: {
controllerId: string
operationId: string
payload: unknown
}): Promise<RemoteRuntimeStopRequest> {
const payload = remoteRuntimeStopPayloadSchema.parse(input.payload)
const identity = await createOperationIdentity({
controllerId: input.controllerId,
operationId: input.operationId,
scope: {
kind: 'run',
sessionId: payload.bindingId,
requestId: payload.requestId
},
method: 'runtime/stopPrompt',
payload
})
return remoteRuntimeStopRequestSchema.parse({ identity, payload })
}
export async function createRemoteRuntimeQueryRequest(input: {
controllerId: string
operationId: string
payload: unknown
}): Promise<RemoteRuntimeQueryRequest> {
const payload = remoteRuntimeQueryPayloadSchema.parse(input.payload)
const identity = await createOperationIdentity({
controllerId: input.controllerId,
operationId: input.operationId,
scope: {
kind: 'run',
sessionId: payload.bindingId,
requestId: payload.requestId
},
method: 'runtime/queryPrompt',
payload
})
return remoteRuntimeQueryRequestSchema.parse({ identity, payload })
}
export async function digestRemoteRuntimeBundleManifest(
manifest: unknown
): Promise<string> {
const parsed = remoteRuntimeBundleManifestSchema.parse(manifest)
return digestCanonicalOperation({
method: 'runtime/bundleManifest',
scope: {
kind: 'installation',
installationId: parsed.runtimeId
},
payload: parsed
})
}
export async function digestRemoteRuntimeBundleIdentity(
manifest: unknown
): Promise<string> {
const parsed = remoteRuntimeBundleManifestSchema.parse(manifest)
const { bundleDigest: _bundleDigest, ...identity } = parsed
void _bundleDigest
return digestCanonicalOperation({
method: 'runtime/bundleIdentity',
scope: {
kind: 'installation',
installationId: parsed.runtimeId
},
payload: identity
})
}
export async function assertDetachedRemoteRuntimeBundleDigest(
bundle: unknown
): Promise<SignedRemoteRuntimeManifest> {
const parsed = detachedSignedRemoteRuntimeBundleSchema.parse(bundle)
const bundleDigest = await digestRemoteRuntimeBundleIdentity(
parsed.manifest
)
if (bundleDigest !== parsed.manifest.bundleDigest) {
throw new Error('Runtime bundle identity digest does not match')
}
const digest = await digestRemoteRuntimeBundleManifest(parsed.manifest)
if (digest !== parsed.detachedSignature.manifestDigest) {
throw new Error('Detached Runtime manifest digest does not match')
}
return parsed
}
export async function assertRemoteRuntimeOperationDigest(
request: RemoteRuntimeStartRequest | RemoteRuntimeStopRequest | RemoteRuntimeQueryRequest
): Promise<void> {
const expectedDigest = await digestCanonicalOperation({
method: request.identity.method,
scope: request.identity.scope,
payload: request.payload
})
if (request.identity.payloadDigest !== expectedDigest) {
throw new Error('Runtime operation payload digest does not match')
}
}
function addOperationBindingIssues(
identity: OperationIdentity,
method: string,
bindingId: string,
requestId: string,
context: z.RefinementCtx
): void {
if (identity.method !== method) {
context.addIssue({
code: 'custom',
path: ['identity', 'method'],
message: `Operation method must be ${method}`
})
}
if (
identity.scope.kind !== 'run' ||
identity.scope.sessionId !== bindingId ||
identity.scope.requestId !== requestId
) {
context.addIssue({
code: 'custom',
path: ['identity', 'scope'],
message: 'Operation scope does not match Runtime ownership'
})
}
}
+372
View File
@@ -0,0 +1,372 @@
import { describe, expect, it } from 'vitest'
import {
SSH_DIRECTORY_BROWSE_LIMITS,
agentBootstrapProbeResultSchema,
sshDirectoryBrowseRequestSchema,
sshDirectoryBrowseResultSchema,
sshHostCreateInputSchema,
sshHostDraftInspectionRequestSchema,
sshHostRemoteEnvironmentSchema,
sshHostValidationRequestSchema
} from './ssh-host-contracts'
describe('SSH host contracts', () => {
it('normalizes bounded display fields and preserves password bytes', () => {
expect(
sshHostCreateInputSchema.parse({
name: ' Build host ',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'password',
password: {
action: 'replace',
value: ' exact password '
}
})
).toEqual({
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'password',
password: {
action: 'replace',
value: ' exact password '
}
})
})
it('requires a password for new password hosts and rejects one for system-agent auth', () => {
expect(() =>
sshHostCreateInputSchema.parse({
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'password',
password: { action: 'keep' }
})
).toThrow()
expect(() =>
sshHostCreateInputSchema.parse({
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'system-agent',
password: { action: 'replace', value: 'not-allowed' }
})
).toThrow()
})
it('rejects whitespace, control characters, unknown fields, and invalid ports', () => {
for (const input of [
{
name: 'Build host',
hostname: 'build host',
port: 22,
username: 'builder',
authentication: 'system-agent',
password: { action: 'clear' }
},
{
name: 'Build\u0000host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'system-agent',
password: { action: 'clear' }
},
{
name: 'Build host',
hostname: 'build.example.com',
port: 65_536,
username: 'builder',
authentication: 'system-agent',
password: { action: 'clear' }
},
{
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'build user',
authentication: 'system-agent',
password: { action: 'clear' },
command: 'rm -rf /'
}
]) {
expect(() => sshHostCreateInputSchema.parse(input)).toThrow()
}
})
it('strictly validates inspection and validation envelopes', () => {
const hostId = '00000000-0000-4000-8000-000000000101'
const candidateId = '00000000-0000-4000-8000-000000000102'
const fingerprint = `SHA256:${'A'.repeat(43)}`
expect(
sshHostDraftInspectionRequestSchema.parse({
hostId,
hostname: 'build.example.com',
port: 22,
username: 'builder'
})
).toEqual({
hostId,
hostname: 'build.example.com',
port: 22,
username: 'builder'
})
expect(() =>
sshHostValidationRequestSchema.parse({
candidateId,
fingerprintSha256: 'SHA256:short',
input: {
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'system-agent',
password: { action: 'clear' }
}
})
).toThrow()
expect(() =>
sshHostValidationRequestSchema.parse({
candidateId,
fingerprintSha256: fingerprint,
input: {
name: 'Build host',
hostname: 'build.example.com',
port: 22,
username: 'builder',
authentication: 'system-agent',
password: { action: 'clear' },
privateKey: 'not-allowed'
}
})
).toThrow()
})
it('strictly validates Agent bootstrap readiness results', () => {
expect(
agentBootstrapProbeResultSchema.parse({
ready: true,
platform: 'linux',
architecture: 'arm64',
canonicalHomeDirectory: '/home/builder',
uid: 1_001,
shell: '/bin/bash',
procfs: 'ready'
})
).toMatchObject({
ready: true,
architecture: 'arm64'
})
expect(
agentBootstrapProbeResultSchema.parse({
ready: false,
reason: 'procfs-unavailable'
})
).toEqual({
ready: false,
reason: 'procfs-unavailable'
})
for (const result of [
{
ready: true,
platform: 'linux',
architecture: 'x64',
canonicalHomeDirectory: '/home/builder/../root',
uid: 1_001,
shell: '/bin/bash',
procfs: 'ready'
},
{
ready: true,
platform: 'linux',
architecture: 'x64',
canonicalHomeDirectory: '/home/builder\u0000',
uid: 1_001,
shell: '/bin/bash',
procfs: 'ready'
},
{
ready: false,
reason: 'procfs-unavailable',
diagnostic: 'environment dump'
}
]) {
expect(() =>
agentBootstrapProbeResultSchema.parse(result)
).toThrow()
}
})
it('exposes only bounded remote environment version metadata', () => {
const status = {
hostId: '00000000-0000-4000-8000-000000000101',
checkedAt: '2030-01-01T00:00:00.000Z',
architecture: 'x64',
agent: {
state: 'update-available',
expected: {
version: '0.11.1',
architecture: 'x64'
},
installed: {
version: '0.10.4',
architecture: 'x64'
}
},
runtimes: [{
runtimeId: 'opencode',
provider: 'opencode',
state: 'current',
expected: {
version: '1.18.9',
architecture: 'x64'
},
installed: {
version: '1.18.9',
architecture: 'x64'
}
}]
}
expect(sshHostRemoteEnvironmentSchema.parse(status)).toEqual(status)
expect(() =>
sshHostRemoteEnvironmentSchema.parse({
...status,
credential: 'secret'
})
).toThrow()
})
it('validates strict directory browse requests and treats an omitted path as valid', () => {
const hostId = '00000000-0000-4000-8000-000000000101'
expect(
sshDirectoryBrowseRequestSchema.parse({ hostId })
).toEqual({ hostId })
expect(
sshDirectoryBrowseRequestSchema.parse({
hostId,
path: '/home/builder'
})
).toEqual({
hostId,
path: '/home/builder'
})
for (const input of [
{ hostId, path: '/home/builder/' },
{ hostId, path: '/home//builder' },
{ hostId, path: '/home/./builder' },
{ hostId, path: '/home/../builder' },
{ hostId, path: '/home/\u0000builder' },
{ hostId, path: `/${'界'.repeat(1_366)}` },
{ hostId, path: '/', command: 'pwd' }
]) {
expect(() =>
sshDirectoryBrowseRequestSchema.parse(input)
).toThrow()
}
})
it('validates deterministic bounded directory browse results', () => {
const result = {
path: '/home/builder',
homeDirectory: '/home/builder',
parentPath: '/home',
entries: [
{
name: 'alpha',
path: '/home/builder/alpha'
},
{
name: '资料',
path: '/home/builder/资料'
}
],
truncated: false
}
expect(
sshDirectoryBrowseResultSchema.parse(result)
).toEqual(result)
expect(
sshDirectoryBrowseResultSchema.parse({
...result,
path: '/',
parentPath: null,
entries: []
})
).toMatchObject({
path: '/',
parentPath: null
})
for (const invalid of [
{
...result,
parentPath: '/'
},
{
...result,
entries: [...result.entries].reverse()
},
{
...result,
entries: [result.entries[0], result.entries[0]]
},
{
...result,
entries: [
{
name: 'elsewhere',
path: '/tmp/elsewhere'
}
]
},
{
...result,
entries: [
{
name: '..',
path: '/home'
}
]
},
{
...result,
entries: [],
credential: 'secret'
}
]) {
expect(() =>
sshDirectoryBrowseResultSchema.parse(invalid)
).toThrow()
}
})
it('caps directory browse output at 500 entries', () => {
const entries = Array.from(
{ length: SSH_DIRECTORY_BROWSE_LIMITS.maximumEntries + 1 },
(_, index) => {
const name = index.toString().padStart(3, '0')
return {
name,
path: `/home/builder/${name}`
}
}
)
expect(() =>
sshDirectoryBrowseResultSchema.parse({
path: '/home/builder',
homeDirectory: '/home/builder',
parentPath: '/home',
entries,
truncated: true
})
).toThrow()
})
})
+464
View File
@@ -0,0 +1,464 @@
import { z } from 'zod'
import { componentVersionSchema } from './agent-installation-contracts'
import { utf8ByteLength } from './agent-protocol/contracts'
export const SSH_HOST_LIMITS = {
maximumHosts: 100,
maximumNameLength: 120,
maximumHostnameLength: 253,
maximumUsernameLength: 128,
maximumPort: 65_535,
maximumPasswordLength: 4_096,
maximumHostKeyLength: 32 * 1_024,
maximumDiagnosticLength: 1_000
} as const
const boundedTextSchema = (
maximum: number,
minimum = 0
): z.ZodString =>
z
.string()
.trim()
.min(minimum)
.max(maximum)
.refine(
(value) =>
Array.from(value).every((character) => {
const codePoint = character.codePointAt(0) ?? 0
return codePoint > 31 && codePoint !== 127
}),
'内容不能包含控制字符'
)
export const sshHostIdSchema = z.string().uuid()
export const sshAuthenticationKindSchema = z.enum([
'password',
'system-agent'
])
export type SshAuthenticationKind = z.infer<
typeof sshAuthenticationKindSchema
>
export const sshPasswordUpdateSchema = z.discriminatedUnion(
'action',
[
z.object({ action: z.literal('keep') }).strict(),
z.object({ action: z.literal('clear') }).strict(),
z
.object({
action: z.literal('replace'),
value: z
.string()
.min(1)
.max(SSH_HOST_LIMITS.maximumPasswordLength)
})
.strict()
]
)
export type SshPasswordUpdate = z.infer<
typeof sshPasswordUpdateSchema
>
const sshHostEditableFields = {
name: boundedTextSchema(SSH_HOST_LIMITS.maximumNameLength, 1),
hostname: boundedTextSchema(
SSH_HOST_LIMITS.maximumHostnameLength,
1
).refine((value) => !/\s/u.test(value), '主机地址不能包含空白字符'),
port: z
.number()
.int()
.min(1)
.max(SSH_HOST_LIMITS.maximumPort),
username: boundedTextSchema(
SSH_HOST_LIMITS.maximumUsernameLength,
1
).refine((value) => !/\s/u.test(value), '用户名不能包含空白字符'),
authentication: sshAuthenticationKindSchema,
password: sshPasswordUpdateSchema
} as const
export const sshHostCreateInputSchema = z
.object(sshHostEditableFields)
.strict()
.superRefine((input, context) => {
if (
input.authentication === 'password' &&
input.password.action !== 'replace'
) {
context.addIssue({
code: 'custom',
path: ['password'],
message: '新建密码认证主机时必须提供密码'
})
}
if (
input.authentication === 'system-agent' &&
input.password.action === 'replace'
) {
context.addIssue({
code: 'custom',
path: ['password'],
message: '系统 SSH Agent 认证不接受密码'
})
}
})
export const sshHostUpdateInputSchema = z
.object(sshHostEditableFields)
.strict()
.superRefine((input, context) => {
if (
input.authentication === 'system-agent' &&
input.password.action === 'replace'
) {
context.addIssue({
code: 'custom',
path: ['password'],
message: '系统 SSH Agent 认证不接受密码'
})
}
})
export type SshHostCreateInput = z.infer<
typeof sshHostCreateInputSchema
>
export type SshHostUpdateInput = z.infer<
typeof sshHostUpdateInputSchema
>
export const sshHostRequestSchema = z
.object({
hostId: sshHostIdSchema
})
.strict()
export const sshHostDraftInspectionRequestSchema = z
.object({
hostId: sshHostIdSchema.optional(),
hostname: sshHostCreateInputSchema.shape.hostname,
port: sshHostCreateInputSchema.shape.port,
username: sshHostCreateInputSchema.shape.username
})
.strict()
export type SshHostDraftInspectionRequest = z.infer<
typeof sshHostDraftInspectionRequestSchema
>
export const sshHostCandidateRequestSchema = z
.object({
candidateId: z.string().uuid()
})
.strict()
export const sshHostValidationRequestSchema = z
.object({
candidateId: z.string().uuid(),
fingerprintSha256: z
.string()
.regex(/^SHA256:[A-Za-z0-9+/]{43}$/u),
input: sshHostUpdateInputSchema
})
.strict()
export type SshHostValidationRequest = z.infer<
typeof sshHostValidationRequestSchema
>
export const sshHostKeyStateSchema = z.enum([
'unverified',
'verified',
'changed'
])
export type SshHostKeyState = z.infer<
typeof sshHostKeyStateSchema
>
export type SshHost = {
id: string
name: string
hostname: string
port: number
username: string
authentication: SshAuthenticationKind
credentialConfigured: boolean
credentialSource:
| 'none'
| 'encrypted'
| 'system-agent'
| 'unreadable'
hostKey: {
state: Exclude<SshHostKeyState, 'changed'>
algorithm?: string
fingerprintSha256?: string
generation: number
}
lastValidatedAt?: string
createdAt: string
updatedAt: string
}
export type SshHostsSnapshot = {
hosts: SshHost[]
secureStorageAvailable: boolean
}
export type SshHostKeyInspection = {
candidateId: string
hostId?: string
state: SshHostKeyState
algorithm: string
fingerprintSha256: string
previousHostKey?: {
algorithm: string
fingerprintSha256: string
}
}
export type SshHostConnectionTestResult = {
hostId: string
connected: true
latencyMs: number
platform: 'linux' | 'darwin' | 'win32' | 'unknown'
architecture: 'x64' | 'arm64' | 'unknown'
shell: string
homeDirectory: string
detail: string
}
export type SshHostValidationResult = {
host: SshHost
connection: SshHostConnectionTestResult
}
export const sshHostRemoteEnvironmentVersionSchema = z
.object({
version: componentVersionSchema,
architecture: z.enum(['x64', 'arm64'])
})
.strict()
export const sshHostRemoteEnvironmentStateSchema = z.enum([
'current',
'update-available',
'not-installed'
])
export const sshHostRemoteEnvironmentSchema = z
.object({
hostId: sshHostIdSchema,
checkedAt: z.string().datetime({ offset: true }),
architecture: z.enum(['x64', 'arm64']),
agent: z
.object({
state: sshHostRemoteEnvironmentStateSchema,
expected: sshHostRemoteEnvironmentVersionSchema,
installed: sshHostRemoteEnvironmentVersionSchema.nullable()
})
.strict(),
runtimes: z
.array(
z
.object({
runtimeId: z.literal('opencode'),
provider: z.literal('opencode'),
state: sshHostRemoteEnvironmentStateSchema,
expected: sshHostRemoteEnvironmentVersionSchema,
installed:
sshHostRemoteEnvironmentVersionSchema.nullable()
})
.strict()
)
.max(16)
})
.strict()
export type SshHostRemoteEnvironment = z.infer<
typeof sshHostRemoteEnvironmentSchema
>
export const agentBootstrapIncompatibleReasonSchema = z.enum([
'non-linux',
'unsupported-architecture',
'home-directory-unavailable',
'uid-unavailable',
'shell-unavailable',
'procfs-unavailable'
])
export type AgentBootstrapIncompatibleReason = z.infer<
typeof agentBootstrapIncompatibleReasonSchema
>
export const canonicalRemotePathSchema = z
.string()
.min(1)
.max(4_096)
.refine(
(value) =>
value.startsWith('/') &&
value !== '/.' &&
value !== '/..' &&
!value.includes('//') &&
!value.split('/').some((part) => part === '.' || part === '..') &&
!/[\p{Cc}\p{Cs}\u2028\u2029\ufffd]/u.test(value),
'远端路径必须是无控制字符的规范绝对路径'
)
export const SSH_DIRECTORY_BROWSE_LIMITS = {
maximumPathBytes: 4_096,
maximumNameBytes: 255,
maximumEntries: 500
} as const
export const sshDirectoryBrowsePathSchema =
canonicalRemotePathSchema.refine(
(value) =>
(value === '/' || !value.endsWith('/')) &&
utf8ByteLength(value) <=
SSH_DIRECTORY_BROWSE_LIMITS.maximumPathBytes,
'远端目录路径必须是规范绝对路径,且不能超过 4096 UTF-8 字节'
)
export const sshDirectoryBrowseRequestSchema = z
.object({
hostId: sshHostIdSchema,
path: sshDirectoryBrowsePathSchema.optional()
})
.strict()
export type SshDirectoryBrowseRequest = z.infer<
typeof sshDirectoryBrowseRequestSchema
>
export const sshDirectoryBrowseEntrySchema = z
.object({
name: z
.string()
.min(1)
.max(255)
.refine(
(value) =>
value !== '.' &&
value !== '..' &&
!value.includes('/') &&
!/[\p{Cc}\p{Cs}\u2028\u2029\ufffd]/u.test(value) &&
utf8ByteLength(value) <=
SSH_DIRECTORY_BROWSE_LIMITS.maximumNameBytes,
'远端目录名称无效或超过 255 UTF-8 字节'
),
path: sshDirectoryBrowsePathSchema
})
.strict()
export type SshDirectoryBrowseEntry = z.infer<
typeof sshDirectoryBrowseEntrySchema
>
function parentRemotePath(path: string): string | null {
if (path === '/') {
return null
}
const separator = path.lastIndexOf('/')
return separator === 0 ? '/' : path.slice(0, separator)
}
export const sshDirectoryBrowseResultSchema = z
.object({
path: sshDirectoryBrowsePathSchema,
homeDirectory: sshDirectoryBrowsePathSchema,
parentPath: sshDirectoryBrowsePathSchema.nullable(),
entries: z
.array(sshDirectoryBrowseEntrySchema)
.max(SSH_DIRECTORY_BROWSE_LIMITS.maximumEntries),
truncated: z.boolean()
})
.strict()
.superRefine((result, context) => {
const expectedParentPath = parentRemotePath(result.path)
if (result.parentPath !== expectedParentPath) {
context.addIssue({
code: 'custom',
path: ['parentPath'],
message: '父目录路径与当前目录不匹配'
})
}
const names = new Set<string>()
const paths = new Set<string>()
let previousName: string | undefined
for (const [index, entry] of result.entries.entries()) {
const expectedPath =
result.path === '/'
? `/${entry.name}`
: `${result.path}/${entry.name}`
if (entry.path !== expectedPath) {
context.addIssue({
code: 'custom',
path: ['entries', index, 'path'],
message: '目录项路径与当前目录或名称不匹配'
})
}
if (names.has(entry.name) || paths.has(entry.path)) {
context.addIssue({
code: 'custom',
path: ['entries', index],
message: '目录项不能重复'
})
}
if (previousName !== undefined && previousName >= entry.name) {
context.addIssue({
code: 'custom',
path: ['entries', index, 'name'],
message: '目录项必须按名称严格升序排列'
})
}
names.add(entry.name)
paths.add(entry.path)
previousName = entry.name
}
})
export type SshDirectoryBrowseResult = z.infer<
typeof sshDirectoryBrowseResultSchema
>
// Keep the schema names aligned with the other host-scoped contracts.
export const sshHostDirectoryBrowseRequestSchema =
sshDirectoryBrowseRequestSchema
export const sshHostDirectoryBrowseResultSchema =
sshDirectoryBrowseResultSchema
export type SshHostDirectoryBrowseRequest = SshDirectoryBrowseRequest
export type SshHostDirectoryBrowseResult = SshDirectoryBrowseResult
export const agentBootstrapProbeResultSchema = z.discriminatedUnion(
'ready',
[
z
.object({
ready: z.literal(true),
platform: z.literal('linux'),
architecture: z.enum(['x64', 'arm64']),
canonicalHomeDirectory: canonicalRemotePathSchema,
uid: z.number().int().min(0).max(4_294_967_294),
shell: canonicalRemotePathSchema,
procfs: z.literal('ready')
})
.strict(),
z
.object({
ready: z.literal(false),
reason: agentBootstrapIncompatibleReasonSchema
})
.strict()
]
)
export type AgentBootstrapProbeResult = z.infer<
typeof agentBootstrapProbeResultSchema
>