feat(protocol): add gateway transport authority contracts

This commit is contained in:
sechmachine
2026-07-29 05:59:46 +07:00
parent b2c24b3fa3
commit b5558133e3
21 changed files with 1898 additions and 15 deletions
+2
View File
@@ -4,3 +4,5 @@ invalid-short 1 datagram hex=564401 invalid:truncated
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
invalid-media-channel 1 datagram hex=5644010d0000000000000000000000000000010000 invalid:unknown_channel
1 id version kind input expected
4 invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
5 invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
6 invalid-length 1 datagram hex=564401010000000000000000000000000000010001 invalid:length_mismatch
7 valid-video-empty 1 datagram hex=5644010a0000000000000000000000000000010000 valid
8 invalid-media-channel 1 datagram hex=5644010d0000000000000000000000000000010000 invalid:unknown_channel
+1 -1
View File
@@ -6,5 +6,5 @@
"fixtures/conformance/events-v1.tsv", "fixtures/conformance/events-v1.tsv",
"fixtures/conformance/tunnel-v1.tsv" "fixtures/conformance/tunnel-v1.tsv"
], ],
"corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761" "corpus_sha256": "0874d39dd14c0107e602ea8909f8d53673f964c67dc9fd5fcadb8641e6ebf592"
} }
+10 -5
View File
@@ -1,7 +1,9 @@
# VerseVDI control datagram v1 # VerseVDI control datagram v1
Phase 3A reserves a bounded control-datagram envelope. It does not forward video, Phase 3C adds bounded encoded video/audio and sequenced-input channels to the same
audio, provider traffic, VM traffic, or arbitrary binary clipboard data. transport envelope. The gateway forwards encoded bytes; it does not decode, encode,
transcode, render, or expose provider traffic. Arbitrary binary clipboard data remains
rejected.
All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes: All multi-byte integers are unsigned big-endian. The fixed header is 21 bytes:
@@ -22,6 +24,9 @@ Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and
length-mismatch frames are rejected before allocation proportional to the claimed length-mismatch frames are rejected before allocation proportional to the claimed
payload. Media/provider identifiers are not registered channels. payload. Media/provider identifiers are not registered channels.
Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and Registered channels are `control.ack.v1`, `control.cancel.v1`, `clipboard.text.v1`,
`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain `media.video.v1`, `media.audio.v1`, and `input.sequenced.v1`. Media/input frames use
subject to the 65,536-byte text limit and explicit authorization. application flow IDs 10, 11, and 12 and a path-MTU-safe payload limit of 1,179 bytes;
larger encoded units use at most 16 validated fragments. Clipboard payloads are UTF-8
JSON text contracts and remain subject to the 65,536-byte text limit and explicit
authorization.
+5 -2
View File
@@ -6,7 +6,10 @@
"channels": [ "channels": [
{"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024}, {"id": 1, "name": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
{"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048}, {"id": 2, "name": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
{"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515} {"id": 3, "name": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65515},
{"id": 10, "name": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
{"id": 11, "name": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1179},
{"id": 12, "name": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1179}
], ],
"reserved_rejected": ["video", "audio", "provider", "vm", "file-transfer", "clipboard.binary"] "reserved_rejected": ["provider", "vm", "file-transfer", "clipboard.binary"]
} }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,5 +14,5 @@
}, },
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c", "generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
"protocol_version": "1.0.0", "protocol_version": "1.0.0",
"schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1" "schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
} }
Binary file not shown.
+102 -1
View File
@@ -1,6 +1,6 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
#![allow(non_snake_case)] #![allow(non_snake_case)]
pub const SCHEMA_SHA256: &str = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"; pub const SCHEMA_SHA256: &str = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec";
pub const CURRENT_WIRE_VERSION: &str = "1"; pub const CURRENT_WIRE_VERSION: &str = "1";
pub const N_MINUS_1_WIRE_VERSION: &str = "0"; pub const N_MINUS_1_WIRE_VERSION: &str = "0";
pub const N_MINUS_2_WIRE_VERSION: &str = "-1"; pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
@@ -46,6 +46,28 @@ pub struct BrokerSession {
pub version: i64, pub version: i64,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityProfile {
pub transport: String,
pub framing: String,
pub media: String,
pub audio: String,
pub sourceRateControl: String,
pub clientDecode: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelFrame {
pub version: String,
pub flowId: String,
pub sequence: i64,
pub flags: i64,
pub fragmentIndex: i64,
pub fragmentCount: i64,
pub timestampMs: i64,
pub payload: String,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardText { pub struct ClipboardText {
pub text: String, pub text: String,
@@ -133,6 +155,43 @@ pub struct FieldViolation {
pub code: String, pub code: String,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayDrain {
pub version: String,
pub gatewayId: String,
pub sequence: i64,
pub reason: String,
pub deadline: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayHeartbeat {
pub version: String,
pub gatewayId: String,
pub sequence: i64,
pub observedAt: String,
pub activeConnections: i64,
pub egressKbps: i64,
pub state: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayRegistration {
pub version: String,
pub gatewayId: String,
pub instanceIdentity: String,
pub certificateIdentity: String,
pub publicIdentity: String,
pub address: String,
pub providerIdentity: String,
pub protocolMinVersion: i64,
pub protocolMaxVersion: i64,
pub connectionCapacity: i64,
pub bandwidthCapacityKbps: i64,
pub features: Vec<String>,
pub capabilities: CapabilityProfile,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReference { pub struct GrantReference {
pub opaqueValue: String, pub opaqueValue: String,
@@ -189,6 +248,15 @@ pub struct PageInfo {
pub nextCursor: String, pub nextCursor: String,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderState {
pub version: String,
pub sessionId: String,
pub state: String,
pub cleanupPending: bool,
pub channels: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReauthGrant { pub struct ReauthGrant {
pub token: String, pub token: String,
@@ -240,6 +308,19 @@ pub struct ResourceList {
pub page: PageInfo, pub page: PageInfo,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionAuthority {
pub version: String,
pub sessionId: String,
pub gatewayId: String,
pub audience: String,
pub reconnectSequence: i64,
pub expiresAt: String,
pub capabilities: CapabilityProfile,
pub providerProfile: String,
pub providerIdentity: String,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionRequest { pub struct SessionRequest {
pub clientDeviceId: String, pub clientDeviceId: String,
@@ -249,6 +330,26 @@ pub struct SessionRequest {
pub policySnapshot: AllocationPolicy, pub policySnapshot: AllocationPolicy,
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StableError {
pub version: String,
pub code: String,
pub message: String,
pub retryable: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TunnelAdmissionRequest {
pub version: String,
pub sessionId: String,
pub gatewayId: String,
pub audience: String,
pub grant: String,
pub reconnectSequence: i64,
pub clientNonce: String,
pub capabilities: CapabilityProfile,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionNegotiation { pub struct VersionNegotiation {
pub supportedVersions: Vec<String>, pub supportedVersions: Vec<String>,
+277 -1
View File
@@ -1,7 +1,7 @@
// Code generated by tools/generate.py; DO NOT EDIT. // Code generated by tools/generate.py; DO NOT EDIT.
import Foundation import Foundation
public typealias JSONObject = [String: String] public typealias JSONObject = [String: String]
public let schemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1" public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
public let currentWireVersion = "1" public let currentWireVersion = "1"
public let nMinus1WireVersion = "0" public let nMinus1WireVersion = "0"
public let nMinus2WireVersion = "-1" public let nMinus2WireVersion = "-1"
@@ -117,6 +117,66 @@ public struct BrokerSession: Codable, Equatable {
} }
} }
public struct CapabilityProfile: Codable, Equatable {
public let transport: String
public let framing: String
public let media: String
public let audio: String
public let sourceRateControl: String
public let clientDecode: String
enum CodingKeys: String, CodingKey {
case transport = "transport"
case framing = "framing"
case media = "media"
case audio = "audio"
case sourceRateControl = "source_rate_control"
case clientDecode = "client_decode"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
transport = try c.decode(String.self, forKey: .transport)
framing = try c.decode(String.self, forKey: .framing)
media = try c.decode(String.self, forKey: .media)
audio = try c.decode(String.self, forKey: .audio)
sourceRateControl = try c.decode(String.self, forKey: .sourceRateControl)
clientDecode = try c.decode(String.self, forKey: .clientDecode)
}
}
public struct ChannelFrame: Codable, Equatable {
public let version: String
public let flowId: String
public let sequence: Int64
public let flags: Int64
public let fragmentIndex: Int64
public let fragmentCount: Int64
public let timestampMs: Int64
public let payload: String
enum CodingKeys: String, CodingKey {
case version = "version"
case flowId = "flow_id"
case sequence = "sequence"
case flags = "flags"
case fragmentIndex = "fragment_index"
case fragmentCount = "fragment_count"
case timestampMs = "timestamp_ms"
case payload = "payload"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
flowId = try c.decode(String.self, forKey: .flowId)
sequence = try c.decode(Int64.self, forKey: .sequence)
flags = try c.decode(Int64.self, forKey: .flags)
fragmentIndex = try c.decode(Int64.self, forKey: .fragmentIndex)
fragmentCount = try c.decode(Int64.self, forKey: .fragmentCount)
timestampMs = try c.decode(Int64.self, forKey: .timestampMs)
payload = try c.decode(String.self, forKey: .payload)
}
}
public struct ClipboardText: Codable, Equatable { public struct ClipboardText: Codable, Equatable {
public let text: String public let text: String
public let encoding: String public let encoding: String
@@ -348,6 +408,108 @@ public struct FieldViolation: Codable, Equatable {
} }
} }
public struct GatewayDrain: Codable, Equatable {
public let version: String
public let gatewayId: String
public let sequence: Int64
public let reason: String
public let deadline: String
enum CodingKeys: String, CodingKey {
case version = "version"
case gatewayId = "gateway_id"
case sequence = "sequence"
case reason = "reason"
case deadline = "deadline"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
gatewayId = try c.decode(String.self, forKey: .gatewayId)
sequence = try c.decode(Int64.self, forKey: .sequence)
reason = try c.decode(String.self, forKey: .reason)
deadline = try c.decode(String.self, forKey: .deadline)
}
}
public struct GatewayHeartbeat: Codable, Equatable {
public let version: String
public let gatewayId: String
public let sequence: Int64
public let observedAt: String
public let activeConnections: Int64
public let egressKbps: Int64
public let state: String
enum CodingKeys: String, CodingKey {
case version = "version"
case gatewayId = "gateway_id"
case sequence = "sequence"
case observedAt = "observed_at"
case activeConnections = "active_connections"
case egressKbps = "egress_kbps"
case state = "state"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
gatewayId = try c.decode(String.self, forKey: .gatewayId)
sequence = try c.decode(Int64.self, forKey: .sequence)
observedAt = try c.decode(String.self, forKey: .observedAt)
activeConnections = try c.decode(Int64.self, forKey: .activeConnections)
egressKbps = try c.decode(Int64.self, forKey: .egressKbps)
state = try c.decode(String.self, forKey: .state)
}
}
public struct GatewayRegistration: Codable, Equatable {
public let version: String
public let gatewayId: String
public let instanceIdentity: String
public let certificateIdentity: String
public let publicIdentity: String
public let address: String
public let providerIdentity: String
public let protocolMinVersion: Int64
public let protocolMaxVersion: Int64
public let connectionCapacity: Int64
public let bandwidthCapacityKbps: Int64
public let features: [String]
public let capabilities: CapabilityProfile
enum CodingKeys: String, CodingKey {
case version = "version"
case gatewayId = "gateway_id"
case instanceIdentity = "instance_identity"
case certificateIdentity = "certificate_identity"
case publicIdentity = "public_identity"
case address = "address"
case providerIdentity = "provider_identity"
case protocolMinVersion = "protocol_min_version"
case protocolMaxVersion = "protocol_max_version"
case connectionCapacity = "connection_capacity"
case bandwidthCapacityKbps = "bandwidth_capacity_kbps"
case features = "features"
case capabilities = "capabilities"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
gatewayId = try c.decode(String.self, forKey: .gatewayId)
instanceIdentity = try c.decode(String.self, forKey: .instanceIdentity)
certificateIdentity = try c.decode(String.self, forKey: .certificateIdentity)
publicIdentity = try c.decode(String.self, forKey: .publicIdentity)
address = try c.decode(String.self, forKey: .address)
providerIdentity = try c.decode(String.self, forKey: .providerIdentity)
protocolMinVersion = try c.decode(Int64.self, forKey: .protocolMinVersion)
protocolMaxVersion = try c.decode(Int64.self, forKey: .protocolMaxVersion)
connectionCapacity = try c.decode(Int64.self, forKey: .connectionCapacity)
bandwidthCapacityKbps = try c.decode(Int64.self, forKey: .bandwidthCapacityKbps)
features = try c.decode([String].self, forKey: .features)
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
}
}
public struct GrantReference: Codable, Equatable { public struct GrantReference: Codable, Equatable {
public let opaqueValue: String public let opaqueValue: String
public let expiresAt: String public let expiresAt: String
@@ -492,6 +654,30 @@ public struct PageInfo: Codable, Equatable {
} }
} }
public struct ProviderState: Codable, Equatable {
public let version: String
public let sessionId: String
public let state: String
public let cleanupPending: Bool
public let channels: [String]
enum CodingKeys: String, CodingKey {
case version = "version"
case sessionId = "session_id"
case state = "state"
case cleanupPending = "cleanup_pending"
case channels = "channels"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
sessionId = try c.decode(String.self, forKey: .sessionId)
state = try c.decode(String.self, forKey: .state)
cleanupPending = try c.decode(Bool.self, forKey: .cleanupPending)
channels = try c.decode([String].self, forKey: .channels)
}
}
public struct ReauthGrant: Codable, Equatable { public struct ReauthGrant: Codable, Equatable {
public let token: String public let token: String
public let purpose: String public let purpose: String
@@ -624,6 +810,42 @@ public struct ResourceList: Codable, Equatable {
} }
} }
public struct SessionAuthority: Codable, Equatable {
public let version: String
public let sessionId: String
public let gatewayId: String
public let audience: String
public let reconnectSequence: Int64
public let expiresAt: String
public let capabilities: CapabilityProfile
public let providerProfile: String
public let providerIdentity: String
enum CodingKeys: String, CodingKey {
case version = "version"
case sessionId = "session_id"
case gatewayId = "gateway_id"
case audience = "audience"
case reconnectSequence = "reconnect_sequence"
case expiresAt = "expires_at"
case capabilities = "capabilities"
case providerProfile = "provider_profile"
case providerIdentity = "provider_identity"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
sessionId = try c.decode(String.self, forKey: .sessionId)
gatewayId = try c.decode(String.self, forKey: .gatewayId)
audience = try c.decode(String.self, forKey: .audience)
reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence)
expiresAt = try c.decode(String.self, forKey: .expiresAt)
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
providerProfile = try c.decode(String.self, forKey: .providerProfile)
providerIdentity = try c.decode(String.self, forKey: .providerIdentity)
}
}
public struct SessionRequest: Codable, Equatable { public struct SessionRequest: Codable, Equatable {
public let clientDeviceId: String public let clientDeviceId: String
public let deviceKeyId: String public let deviceKeyId: String
@@ -648,6 +870,60 @@ public struct SessionRequest: Codable, Equatable {
} }
} }
public struct StableError: Codable, Equatable {
public let version: String
public let code: String
public let message: String
public let retryable: Bool
enum CodingKeys: String, CodingKey {
case version = "version"
case code = "code"
case message = "message"
case retryable = "retryable"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
code = try c.decode(String.self, forKey: .code)
message = try c.decode(String.self, forKey: .message)
retryable = try c.decode(Bool.self, forKey: .retryable)
}
}
public struct TunnelAdmissionRequest: Codable, Equatable {
public let version: String
public let sessionId: String
public let gatewayId: String
public let audience: String
public let grant: String
public let reconnectSequence: Int64
public let clientNonce: String
public let capabilities: CapabilityProfile
enum CodingKeys: String, CodingKey {
case version = "version"
case sessionId = "session_id"
case gatewayId = "gateway_id"
case audience = "audience"
case grant = "grant"
case reconnectSequence = "reconnect_sequence"
case clientNonce = "client_nonce"
case capabilities = "capabilities"
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
version = try c.decode(String.self, forKey: .version)
sessionId = try c.decode(String.self, forKey: .sessionId)
gatewayId = try c.decode(String.self, forKey: .gatewayId)
audience = try c.decode(String.self, forKey: .audience)
grant = try c.decode(String.self, forKey: .grant)
reconnectSequence = try c.decode(Int64.self, forKey: .reconnectSequence)
clientNonce = try c.decode(String.self, forKey: .clientNonce)
capabilities = try c.decode(CapabilityProfile.self, forKey: .capabilities)
}
}
public struct VersionNegotiation: Codable, Equatable { public struct VersionNegotiation: Codable, Equatable {
public let supportedVersions: [String] public let supportedVersions: [String]
public let features: [String] public let features: [String]
@@ -0,0 +1,24 @@
## Decisions
- Use the existing generated JSON binding pipeline and add only object definitions that
consumers need now.
- Keep registration/authority messages distinct from `ConnectionManifest`; the latter is
client-facing and contains no provider route, certificate, identity, or credential.
- Encode capability domains as bounded registered strings (`transport`, `framing`, `media`,
`audio`, `source_rate_control`, `client_decode`) so unknown or empty required domains
cannot silently fall back.
- Use a fixed 21-byte big-endian datagram header with application flow IDs and a payload
limit below the path MTU; control channels 1-3 remain compatible with Phase 3A.
## Bounds and failure behavior
All arrays, strings, payloads, fragments, and timestamps are bounded by the JSON schema or
frame registry. Generated decoders reject unknown fields, trailing values, invalid versions,
and missing required fields. Provider address, RTSP, credential, and private-key names are
not added to client-facing definitions.
## Compatibility
The current wire version remains `1`; current, N-1, and N-2 declarations remain unchanged.
New identifiers are additive. Consumers must reject an unknown major or no-overlap selection
before provider launch.
@@ -0,0 +1,32 @@
## Why
Phase 3A defines gateway-only manifests and control datagrams but has no versioned
contracts for the separately deployed gateway's registration, admission authority,
capability intersection, lifecycle, or encoded relay framing. Phase 3C-G needs those
contracts frozen before Data Plane or Connection Server consumers change.
## What Changes
- Add bounded JSON bindings for gateway registration, heartbeat/drain, tunnel admission,
session-scoped authority, capability selection, channel framing, provider state, and
stable errors.
- Extend the tunnel protobuf descriptor with the same versioned control messages.
- Register only gateway-owned media/input flow identifiers; keep provider endpoints and
credentials out of client-facing manifests.
## Ownership and provenance
Protocol owns the wire contract. The contract is original VerseVDI work derived from the
Phase 3A schemas and public Apollo/Moonlight behavior recorded in the Planning Hub. No
GPL source is copied into this repository.
## Non-goals
- Provider implementation, database policy, QUIC implementation, media decoding, or client
rendering.
- Direct client-to-Apollo routing, provider endpoint exposure, or a speculative plugin ABI.
## Stop conditions
Unknown versions, malformed bounds, no capability overlap, downgrade without explicit
acknowledgement, forbidden provider fields, and oversized frames fail closed.
@@ -0,0 +1,37 @@
## ADDED Requirements
### Requirement: Versioned gateway authority contracts
Gateway registration, heartbeat, drain, admission, capability selection, channel framing,
provider state, and stable errors SHALL use bounded versioned objects with strict decoding.
#### Scenario: Unknown or malformed gateway message
- **WHEN** a consumer receives an unknown version, missing required field, unknown field,
invalid bound, or trailing JSON value
- **THEN** it rejects the message before allocating provider or media state with a stable
validation error.
### Requirement: Gateway-only client manifest
Client-facing manifests SHALL expose only the selected gateway, tunnel/profile identifiers,
policy bounds, opaque grant, audience, expiry, session, and correlation data.
#### Scenario: Provider route injection
- **WHEN** a manifest or client-facing authority contains a provider address, RTSP URL,
certificate, pairing identity, credential, or private key field
- **THEN** schema validation rejects it and no generated client binding accepts it.
### Requirement: Explicit capability intersection
Transport, framing, media, audio, source-rate-control, and client-decode capabilities SHALL
remain separate and no-overlap or unknown-required-profile results SHALL fail closed.
#### Scenario: No capability overlap
- **WHEN** policy, gateway, provider, and client capabilities have no allowed intersection
- **THEN** negotiation returns a stable no-overlap error before provider launch or media flow.
### Requirement: Bounded encoded datagrams
Encoded media and approved sequenced input SHALL use registered application flow identifiers,
validated fixed headers, bounded fragments, and payload bytes that are not codec-transformed.
#### Scenario: Malformed or oversized datagram
- **WHEN** a datagram is truncated, has an unknown flow, invalid fragment, length mismatch,
or exceeds its registered payload limit
- **THEN** the datagram is rejected without allocation proportional to the claimed payload.
@@ -0,0 +1,5 @@
- [x] Add versioned gateway and authority definitions to the schema and tunnel descriptor.
- [x] Add registered media/audio/sequenced-input flow identifiers and positive/negative fixtures.
- [x] Regenerate Go/Rust/Swift bindings and descriptor outputs.
- [x] Run strict validation, cross-language conformance, and deterministic fixture hashing.
- [ ] Freeze the local Protocol candidate commit and record its hash for consumers.
+92
View File
@@ -49,3 +49,95 @@ message ChannelOpen {
string direction = 2; string direction = 2;
uint32 maximum_frame_bytes = 3; uint32 maximum_frame_bytes = 3;
} }
message CapabilityProfile {
string transport = 1;
string framing = 2;
string media = 3;
string audio = 4;
string source_rate_control = 5;
string client_decode = 6;
}
message GatewayRegistration {
string version = 1;
string gateway_id = 2;
string instance_identity = 3;
string certificate_identity = 4;
string public_identity = 5;
string address = 6;
uint32 protocol_min_version = 7;
uint32 protocol_max_version = 8;
uint32 connection_capacity = 9;
uint64 bandwidth_capacity_kbps = 10;
repeated string features = 11;
CapabilityProfile capabilities = 12;
string provider_identity = 13;
}
message GatewayHeartbeat {
string version = 1;
string gateway_id = 2;
uint64 sequence = 3;
google.protobuf.Timestamp observed_at = 4;
uint32 active_connections = 5;
uint64 egress_kbps = 6;
string state = 7;
}
message GatewayDrain {
string version = 1;
string gateway_id = 2;
uint64 sequence = 3;
string reason = 4;
google.protobuf.Timestamp deadline = 5;
}
message TunnelAdmissionRequest {
string version = 1;
string session_id = 2;
string gateway_id = 3;
string audience = 4;
string grant = 5;
uint64 reconnect_sequence = 6;
string client_nonce = 7;
CapabilityProfile capabilities = 8;
}
message SessionAuthority {
string version = 1;
string session_id = 2;
string gateway_id = 3;
string audience = 4;
uint64 reconnect_sequence = 5;
google.protobuf.Timestamp expires_at = 6;
CapabilityProfile capabilities = 7;
string provider_profile = 8;
string provider_identity = 9;
}
message ChannelFrame {
string version = 1;
string flow_id = 2;
uint64 sequence = 3;
uint32 flags = 4;
uint32 fragment_index = 5;
uint32 fragment_count = 6;
uint64 timestamp_ms = 7;
bytes payload = 8;
}
message ProviderState {
string version = 1;
string session_id = 2;
string state = 3;
bool cleanup_pending = 4;
repeated string channels = 5;
}
message StableError {
string version = 1;
string code = 2;
string message = 3;
bool retryable = 4;
}
+4 -1
View File
@@ -4,6 +4,9 @@
"datagrams": [ "datagrams": [
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024}, {"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
{"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048}, {"id": "control.cancel.v1", "direction": "client-to-server", "max_payload_bytes": 2048},
{"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536} {"id": "clipboard.text.v1", "direction": "bidirectional", "max_payload_bytes": 65536},
{"id": "media.video.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
{"id": "media.audio.v1", "direction": "server-to-client", "max_payload_bytes": 1200},
{"id": "input.sequenced.v1", "direction": "client-to-server", "max_payload_bytes": 1200}
] ]
} }
+128
View File
@@ -343,6 +343,134 @@
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}}, "supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}} "features": {"type": "array", "maxItems": 64, "items": {"type": "string", "maxLength": 64}}
} }
},
"CapabilityProfile": {
"type": "object",
"additionalProperties": false,
"required": ["transport", "framing", "media", "audio", "source_rate_control", "client_decode"],
"properties": {
"transport": {"type": "string", "minLength": 1, "maxLength": 64},
"framing": {"type": "string", "minLength": 1, "maxLength": 64},
"media": {"type": "string", "minLength": 1, "maxLength": 64},
"audio": {"type": "string", "minLength": 1, "maxLength": 64},
"source_rate_control": {"type": "string", "minLength": 1, "maxLength": 64},
"client_decode": {"type": "string", "minLength": 1, "maxLength": 64}
}
},
"GatewayRegistration": {
"type": "object",
"additionalProperties": false,
"required": ["version", "gateway_id", "instance_identity", "certificate_identity", "public_identity", "address", "provider_identity", "protocol_min_version", "protocol_max_version", "connection_capacity", "bandwidth_capacity_kbps", "features", "capabilities"],
"properties": {
"version": {"type": "string", "const": "1"},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"instance_identity": {"type": "string", "minLength": 1, "maxLength": 512},
"certificate_identity": {"type": "string", "minLength": 1, "maxLength": 512},
"public_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"address": {"type": "string", "minLength": 1, "maxLength": 256},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256},
"protocol_min_version": {"type": "integer", "minimum": 1, "maximum": 100},
"protocol_max_version": {"type": "integer", "minimum": 1, "maximum": 100},
"connection_capacity": {"type": "integer", "minimum": 1, "maximum": 1000000},
"bandwidth_capacity_kbps": {"type": "integer", "minimum": 1, "maximum": 1000000000},
"features": {"type": "array", "maxItems": 64, "items": {"type": "string", "minLength": 1, "maxLength": 64}},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
}
},
"GatewayHeartbeat": {
"type": "object",
"additionalProperties": false,
"required": ["version", "gateway_id", "sequence", "observed_at", "active_connections", "egress_kbps", "state"],
"properties": {
"version": {"type": "string", "const": "1"},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"sequence": {"type": "integer", "minimum": 1},
"observed_at": {"type": "string", "format": "date-time", "maxLength": 64},
"active_connections": {"type": "integer", "minimum": 0, "maximum": 1000000},
"egress_kbps": {"type": "integer", "minimum": 0, "maximum": 1000000000},
"state": {"type": "string", "enum": ["ready", "draining", "offline"]}
}
},
"GatewayDrain": {
"type": "object",
"additionalProperties": false,
"required": ["version", "gateway_id", "sequence", "reason", "deadline"],
"properties": {
"version": {"type": "string", "const": "1"},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"sequence": {"type": "integer", "minimum": 1},
"reason": {"type": "string", "minLength": 1, "maxLength": 256},
"deadline": {"type": "string", "format": "date-time", "maxLength": 64}
}
},
"TunnelAdmissionRequest": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "grant", "reconnect_sequence", "client_nonce", "capabilities"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
"grant": {"type": "string", "minLength": 43, "maxLength": 256},
"reconnect_sequence": {"type": "integer", "minimum": 0},
"client_nonce": {"type": "string", "minLength": 16, "maxLength": 128},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"}
}
},
"SessionAuthority": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "gateway_id", "audience", "reconnect_sequence", "expires_at", "capabilities", "provider_profile", "provider_identity"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"gateway_id": {"type": "string", "minLength": 1, "maxLength": 128},
"audience": {"type": "string", "minLength": 1, "maxLength": 256},
"reconnect_sequence": {"type": "integer", "minimum": 0},
"expires_at": {"type": "string", "format": "date-time", "maxLength": 64},
"capabilities": {"$ref": "#/$defs/CapabilityProfile"},
"provider_profile": {"type": "string", "enum": ["apollo"]},
"provider_identity": {"type": "string", "minLength": 1, "maxLength": 256}
}
},
"ChannelFrame": {
"type": "object",
"additionalProperties": false,
"required": ["version", "flow_id", "sequence", "flags", "fragment_index", "fragment_count", "timestamp_ms", "payload"],
"properties": {
"version": {"type": "string", "const": "1"},
"flow_id": {"type": "string", "minLength": 1, "maxLength": 64},
"sequence": {"type": "integer", "minimum": 0},
"flags": {"type": "integer", "minimum": 0, "maximum": 255},
"fragment_index": {"type": "integer", "minimum": 0, "maximum": 15},
"fragment_count": {"type": "integer", "minimum": 1, "maximum": 16},
"timestamp_ms": {"type": "integer", "minimum": 0},
"payload": {"type": "string", "maxLength": 87384, "x-max-bytes": 65536}
}
},
"ProviderState": {
"type": "object",
"additionalProperties": false,
"required": ["version", "session_id", "state", "cleanup_pending", "channels"],
"properties": {
"version": {"type": "string", "const": "1"},
"session_id": {"type": "string", "minLength": 1, "maxLength": 128},
"state": {"type": "string", "enum": ["starting", "ready", "disconnected", "terminating", "terminated", "cleanup_pending", "failed"]},
"cleanup_pending": {"type": "boolean"},
"channels": {"type": "array", "maxItems": 8, "items": {"type": "string", "minLength": 1, "maxLength": 64}}
}
},
"StableError": {
"type": "object",
"additionalProperties": false,
"required": ["version", "code", "message", "retryable"],
"properties": {
"version": {"type": "string", "const": "1"},
"code": {"type": "string", "minLength": 1, "maxLength": 128},
"message": {"type": "string", "minLength": 1, "maxLength": 512},
"retryable": {"type": "boolean"}
}
} }
} }
} }
+25
View File
@@ -35,3 +35,28 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
t.Fatal("DecodeErrorEnvelope accepted a missing required boolean") t.Fatal("DecodeErrorEnvelope accepted a missing required boolean")
} }
} }
func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
registration := `{"version":"1","gateway_id":"gateway-1","instance_identity":"instance-1","certificate_identity":"cert-1","public_identity":"public-1","address":"gateway.test:443","provider_identity":"apollo-provider-1","protocol_min_version":1,"protocol_max_version":1,"connection_capacity":8,"bandwidth_capacity_kbps":100000,"features":["datagram.media"],"capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"}}`
if _, err := protocol.DecodeGatewayRegistration([]byte(registration)); err != nil {
t.Fatalf("valid gateway registration rejected: %v", err)
}
for _, invalid := range []string{
strings.Replace(registration, `"version":"1"`, `"version":"2"`, 1),
strings.Replace(registration, `"features":["datagram.media"]`, `"features":["datagram.media"],"provider_url":"https://provider.invalid"`, 1),
} {
if _, err := protocol.DecodeGatewayRegistration([]byte(invalid)); err == nil {
t.Fatalf("invalid gateway registration accepted: %s", invalid)
}
}
}
func TestSessionAuthorityRejectsProviderRoute(t *testing.T) {
valid := `{"version":"1","session_id":"session-1","gateway_id":"gateway-1","audience":"versevdi-gateway","reconnect_sequence":0,"expires_at":"2099-01-01T00:00:00Z","capabilities":{"transport":"quic","framing":"datagram-v1","media":"encoded","audio":"encoded","source_rate_control":"server","client_decode":"h264-opus"},"provider_profile":"apollo","provider_identity":"provider-1"}`
if _, err := protocol.DecodeSessionAuthority([]byte(valid)); err != nil {
t.Fatalf("valid session authority rejected: %v", err)
}
if _, err := protocol.DecodeSessionAuthority([]byte(strings.Replace(valid, `"provider_identity":"provider-1"`, `"provider_identity":"provider-1","rtsp_url":"rtsp://provider.invalid"`, 1))); err == nil {
t.Fatal("session authority accepted a provider route")
}
}
+1 -1
View File
@@ -157,7 +157,7 @@ func classifyDatagram(encoded string) string {
if raw[2] != 1 { if raw[2] != 1 {
return "invalid:unsupported_version" return "invalid:unsupported_version"
} }
limits := map[byte]int{1: 1024, 2: 2048, 3: 65515} limits := map[byte]int{1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
limit, ok := limits[raw[3]] limit, ok := limits[raw[3]]
if !ok { if !ok {
return "invalid:unknown_channel" return "invalid:unknown_channel"
+1
View File
@@ -85,6 +85,7 @@ fn classify_datagram(encoded: &str) -> &'static str {
1 => 1024, 1 => 1024,
2 => 2048, 2 => 2048,
3 => 65515, 3 => 65515,
10 | 11 | 12 => 1179,
_ => return "invalid:unknown_channel", _ => return "invalid:unknown_channel",
}; };
if raw[4] != 0 { if raw[4] != 0 {
+1
View File
@@ -50,6 +50,7 @@ func classifyDatagram(_ encoded: String) -> String {
case 1: limit = 1024 case 1: limit = 1024
case 2: limit = 2048 case 2: limit = 2048
case 3: limit = 65515 case 3: limit = 65515
case 10, 11, 12: limit = 1179
default: return "invalid:unknown_channel" default: return "invalid:unknown_channel"
} }
guard raw[4] == 0 else { return "invalid:flags" } guard raw[4] == 0 else { return "invalid:flags" }
+1 -1
View File
@@ -9,7 +9,7 @@ import pathlib
ROOT = pathlib.Path(__file__).resolve().parents[1] ROOT = pathlib.Path(__file__).resolve().parents[1]
HEADER_BYTES = 21 HEADER_BYTES = 21
MAX_FRAME_BYTES = 65536 MAX_FRAME_BYTES = 65536
CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515} CHANNEL_LIMITS = {1: 1024, 2: 2048, 3: 65515, 10: 1179, 11: 1179, 12: 1179}
def classify(raw: bytes) -> str: def classify(raw: bytes) -> str: