feat(protocol): add gateway transport authority contracts
This commit is contained in:
@@ -4,3 +4,5 @@ invalid-short 1 datagram hex=564401 invalid:truncated
|
||||
invalid-version 1 datagram hex=564402010000000000000000000000000000010000 invalid:unsupported_version
|
||||
invalid-channel 1 datagram hex=564401990000000000000000000000000000010000 invalid:unknown_channel
|
||||
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
|
||||
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"fixtures/conformance/events-v1.tsv",
|
||||
"fixtures/conformance/tunnel-v1.tsv"
|
||||
],
|
||||
"corpus_sha256": "c91a512dc67aa9912b31b21144be2adfeacf0dc80dd8515bd3b4a8f52977e761"
|
||||
"corpus_sha256": "0874d39dd14c0107e602ea8909f8d53673f964c67dc9fd5fcadb8641e6ebf592"
|
||||
}
|
||||
|
||||
+10
-5
@@ -1,7 +1,9 @@
|
||||
# VerseVDI control datagram v1
|
||||
|
||||
Phase 3A reserves a bounded control-datagram envelope. It does not forward video,
|
||||
audio, provider traffic, VM traffic, or arbitrary binary clipboard data.
|
||||
Phase 3C adds bounded encoded video/audio and sequenced-input channels to the same
|
||||
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:
|
||||
|
||||
@@ -22,6 +24,9 @@ Truncated, oversized, unknown-version, unknown-channel, invalid-fragment, and
|
||||
length-mismatch frames are rejected before allocation proportional to the claimed
|
||||
payload. Media/provider identifiers are not registered channels.
|
||||
|
||||
Registered Phase 3A channels are `control.ack.v1`, `control.cancel.v1`, and
|
||||
`clipboard.text.v1`. Clipboard payloads are UTF-8 JSON text contracts and remain
|
||||
subject to the 65,536-byte text limit and explicit authorization.
|
||||
Registered channels are `control.ack.v1`, `control.cancel.v1`, `clipboard.text.v1`,
|
||||
`media.video.v1`, `media.audio.v1`, and `input.sequenced.v1`. Media/input frames use
|
||||
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.
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"channels": [
|
||||
{"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": 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"]
|
||||
}
|
||||
|
||||
+1149
-1
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,5 +14,5 @@
|
||||
},
|
||||
"generator_sha256": "cb975bcd42bf77641b6a0f44d5ec7a6fdba1858d6b8a865e0f04e53bad82648c",
|
||||
"protocol_version": "1.0.0",
|
||||
"schema_sha256": "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
||||
"schema_sha256": "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
||||
}
|
||||
|
||||
Binary file not shown.
+102
-1
@@ -1,6 +1,6 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
#![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 N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||
@@ -46,6 +46,28 @@ pub struct BrokerSession {
|
||||
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)]
|
||||
pub struct ClipboardText {
|
||||
pub text: String,
|
||||
@@ -133,6 +155,43 @@ pub struct FieldViolation {
|
||||
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)]
|
||||
pub struct GrantReference {
|
||||
pub opaqueValue: String,
|
||||
@@ -189,6 +248,15 @@ pub struct PageInfo {
|
||||
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)]
|
||||
pub struct ReauthGrant {
|
||||
pub token: String,
|
||||
@@ -240,6 +308,19 @@ pub struct ResourceList {
|
||||
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)]
|
||||
pub struct SessionRequest {
|
||||
pub clientDeviceId: String,
|
||||
@@ -249,6 +330,26 @@ pub struct SessionRequest {
|
||||
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)]
|
||||
pub struct VersionNegotiation {
|
||||
pub supportedVersions: Vec<String>,
|
||||
|
||||
+277
-1
@@ -1,7 +1,7 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
import Foundation
|
||||
public typealias JSONObject = [String: String]
|
||||
public let schemaSHA256 = "36e4c8bac2eae674c1eba551c6ca8c64bf80fcc092ca63ec89a2c71c2bec86e1"
|
||||
public let schemaSHA256 = "b8a69785112bb94d45f47c2250ca59d0bde47e3667b8ad89c9b0e2c4cfb25aec"
|
||||
public let currentWireVersion = "1"
|
||||
public let nMinus1WireVersion = "0"
|
||||
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 let text: 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 let opaqueValue: 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 let token: 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 let clientDeviceId: 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 let supportedVersions: [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.
|
||||
+37
@@ -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.
|
||||
@@ -49,3 +49,95 @@ message ChannelOpen {
|
||||
string direction = 2;
|
||||
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,6 +4,9 @@
|
||||
"datagrams": [
|
||||
{"id": "control.ack.v1", "direction": "bidirectional", "max_payload_bytes": 1024},
|
||||
{"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}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -343,6 +343,134 @@
|
||||
"supported_versions": {"type": "array", "minItems": 1, "maxItems": 3, "items": {"type": "string", "maxLength": 16}},
|
||||
"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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,3 +35,28 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ func classifyDatagram(encoded string) string {
|
||||
if raw[2] != 1 {
|
||||
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]]
|
||||
if !ok {
|
||||
return "invalid:unknown_channel"
|
||||
|
||||
@@ -85,6 +85,7 @@ fn classify_datagram(encoded: &str) -> &'static str {
|
||||
1 => 1024,
|
||||
2 => 2048,
|
||||
3 => 65515,
|
||||
10 | 11 | 12 => 1179,
|
||||
_ => return "invalid:unknown_channel",
|
||||
};
|
||||
if raw[4] != 0 {
|
||||
|
||||
@@ -50,6 +50,7 @@ func classifyDatagram(_ encoded: String) -> String {
|
||||
case 1: limit = 1024
|
||||
case 2: limit = 2048
|
||||
case 3: limit = 65515
|
||||
case 10, 11, 12: limit = 1179
|
||||
default: return "invalid:unknown_channel"
|
||||
}
|
||||
guard raw[4] == 0 else { return "invalid:flags" }
|
||||
|
||||
@@ -9,7 +9,7 @@ import pathlib
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
HEADER_BYTES = 21
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user