feat(protocol): negotiate registered gateway profiles
This commit is contained in:
+42
-14
@@ -13,7 +13,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const SchemaSHA256 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
const ProtocolVersion = "1.0.0"
|
||||
const CurrentWireVersion = "1"
|
||||
const NMinus1WireVersion = "0"
|
||||
@@ -68,12 +68,12 @@ type BrokerSession struct {
|
||||
}
|
||||
|
||||
type CapabilityProfile struct {
|
||||
Transport string `json:"transport"`
|
||||
Framing string `json:"framing"`
|
||||
Media string `json:"media"`
|
||||
Audio string `json:"audio"`
|
||||
SourceRateControl string `json:"source_rate_control"`
|
||||
ClientDecode string `json:"client_decode"`
|
||||
Transport string `json:"transport"`
|
||||
Framing string `json:"framing"`
|
||||
Media string `json:"media"`
|
||||
Audio string `json:"audio"`
|
||||
SourceRateControl string `json:"source_rate_control"`
|
||||
ClientDecode []string `json:"client_decode"`
|
||||
}
|
||||
|
||||
type ChannelFrame struct {
|
||||
@@ -884,14 +884,26 @@ func (v CapabilityProfile) Validate() error {
|
||||
if len(v.SourceRateControl) > 64 {
|
||||
violations = append(violations, FieldViolation{Field: "source_rate_control", Code: "max_length"})
|
||||
}
|
||||
if v.ClientDecode == "" {
|
||||
if v.ClientDecode == nil {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "required"})
|
||||
}
|
||||
if len(v.ClientDecode) < 1 && v.ClientDecode != "" {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_length"})
|
||||
if len(v.ClientDecode) < 1 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_items"})
|
||||
}
|
||||
if len(v.ClientDecode) > 64 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_length"})
|
||||
if len(v.ClientDecode) > 2 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_items"})
|
||||
}
|
||||
for _, item := range v.ClientDecode {
|
||||
if !(item == "h264-opus" || item == "hevc-opus") {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "invalid_item"})
|
||||
}
|
||||
}
|
||||
for index, item := range v.ClientDecode {
|
||||
for prior := 0; prior < index; prior++ {
|
||||
if item == v.ClientDecode[prior] {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "duplicate_item"})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
@@ -4990,16 +5002,32 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
|
||||
if err := selected.Validate(); err != nil {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
common := append([]string(nil), selected.ClientDecode...)
|
||||
for _, profile := range profiles[1:] {
|
||||
if err := profile.Validate(); err != nil || profile != selected {
|
||||
if err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
next := common[:0]
|
||||
for _, candidate := range common {
|
||||
for _, offered := range profile.ClientDecode {
|
||||
if candidate == offered {
|
||||
next = append(next, candidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
common = next
|
||||
if len(common) == 0 {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
}
|
||||
selected.ClientDecode = common
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {
|
||||
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}
|
||||
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf("%d", len(v.Capabilities.ClientDecode))}
|
||||
fields = append(fields, v.Capabilities.ClientDecode...)
|
||||
var transcript strings.Builder
|
||||
transcript.WriteString("versevdi/tunnel-admission/v1")
|
||||
for _, field := range fields {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
"2"
|
||||
]
|
||||
},
|
||||
"generator_sha256": "922983e07a8ecc559771778fbf139155b14664742d9873be062880102777dccb",
|
||||
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
|
||||
"protocol_version": "1.0.0",
|
||||
"schema_sha256": "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
}
|
||||
|
||||
Binary file not shown.
+15
-10
@@ -1,6 +1,6 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
#![allow(non_snake_case)]
|
||||
pub const SCHEMA_SHA256: &str = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b";
|
||||
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
|
||||
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";
|
||||
@@ -210,11 +210,11 @@ pub struct CapabilityProfile {
|
||||
media: String,
|
||||
audio: String,
|
||||
sourceRateControl: String,
|
||||
clientDecode: String,
|
||||
clientDecode: Vec<String>,
|
||||
}
|
||||
|
||||
impl CapabilityProfile {
|
||||
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) -> Result<Self, ValidationError> {
|
||||
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: Vec<String>) -> Result<Self, ValidationError> {
|
||||
let value = Self { transport, framing, media, audio, sourceRateControl, clientDecode };
|
||||
value.validate()?;
|
||||
Ok(value)
|
||||
@@ -235,9 +235,10 @@ impl CapabilityProfile {
|
||||
if self.sourceRateControl.is_empty() { return Err(ValidationError::new("source_rate_control", "required")); }
|
||||
if !self.sourceRateControl.is_empty() && self.sourceRateControl.len() < 1 { return Err(ValidationError::new("source_rate_control", "min_length")); }
|
||||
if self.sourceRateControl.len() > 64 { return Err(ValidationError::new("source_rate_control", "max_length")); }
|
||||
if self.clientDecode.is_empty() { return Err(ValidationError::new("client_decode", "required")); }
|
||||
if !self.clientDecode.is_empty() && self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_length")); }
|
||||
if self.clientDecode.len() > 64 { return Err(ValidationError::new("client_decode", "max_length")); }
|
||||
if self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_items")); }
|
||||
if self.clientDecode.len() > 2 { return Err(ValidationError::new("client_decode", "max_items")); }
|
||||
for item in self.clientDecode.iter() { if item != "h264-opus" && item != "hevc-opus" { return Err(ValidationError::new("client_decode", "invalid_item")); } }
|
||||
for (index, item) in self.clientDecode.iter().enumerate() { if self.clientDecode[..index].contains(item) { return Err(ValidationError::new("client_decode", "duplicate_item")); } }
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> &String { &self.transport }
|
||||
@@ -245,7 +246,7 @@ impl CapabilityProfile {
|
||||
pub fn media(&self) -> &String { &self.media }
|
||||
pub fn audio(&self) -> &String { &self.audio }
|
||||
pub fn sourceRateControl(&self) -> &String { &self.sourceRateControl }
|
||||
pub fn clientDecode(&self) -> &String { &self.clientDecode }
|
||||
pub fn clientDecode(&self) -> &Vec<String> { &self.clientDecode }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1729,7 +1730,9 @@ impl TunnelAdmissionRequest {
|
||||
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
|
||||
pub fn device_admission_transcript(&self) -> Vec<u8> {
|
||||
let reconnect_sequence = self.reconnectSequence.to_string();
|
||||
let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];
|
||||
let client_decode_count = self.capabilities.clientDecode.len().to_string();
|
||||
let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];
|
||||
fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));
|
||||
let mut transcript = String::from("versevdi/tunnel-admission/v1");
|
||||
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
|
||||
transcript.into_bytes()
|
||||
@@ -1759,11 +1762,13 @@ impl VersionNegotiation {
|
||||
}
|
||||
|
||||
pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {
|
||||
let selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
|
||||
let mut selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
|
||||
selected.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
|
||||
for profile in &profiles[1..] {
|
||||
profile.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
|
||||
if profile != &selected { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));
|
||||
if selected.clientDecode.is_empty() { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
+15
-10
@@ -1,7 +1,7 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
import Foundation
|
||||
public typealias JSONObject = [String: String]
|
||||
public let schemaSHA256 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
public let currentWireVersion = "1"
|
||||
public let nMinus1WireVersion = "0"
|
||||
public let nMinus2WireVersion = "-1"
|
||||
@@ -247,7 +247,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
public let media: String
|
||||
public let audio: String
|
||||
public let sourceRateControl: String
|
||||
public let clientDecode: String
|
||||
public let clientDecode: [String]
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case transport = "transport"
|
||||
case framing = "framing"
|
||||
@@ -257,7 +257,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
case clientDecode = "client_decode"
|
||||
}
|
||||
|
||||
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) throws {
|
||||
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: [String]) throws {
|
||||
self.transport = transport
|
||||
self.framing = framing
|
||||
self.media = media
|
||||
@@ -271,7 +271,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
let all = try decoder.container(keyedBy: AnyCodingKey.self)
|
||||
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
try self.init(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))
|
||||
try self.init(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 func validate() throws {
|
||||
@@ -290,9 +290,10 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
if self.sourceRateControl.isEmpty { throw ContractValidationError(field: "source_rate_control", code: "required") }
|
||||
if !self.sourceRateControl.isEmpty && self.sourceRateControl.utf8.count < 1 { throw ContractValidationError(field: "source_rate_control", code: "min_length") }
|
||||
if self.sourceRateControl.utf8.count > 64 { throw ContractValidationError(field: "source_rate_control", code: "max_length") }
|
||||
if self.clientDecode.isEmpty { throw ContractValidationError(field: "client_decode", code: "required") }
|
||||
if !self.clientDecode.isEmpty && self.clientDecode.utf8.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_length") }
|
||||
if self.clientDecode.utf8.count > 64 { throw ContractValidationError(field: "client_decode", code: "max_length") }
|
||||
if self.clientDecode.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_items") }
|
||||
if self.clientDecode.count > 2 { throw ContractValidationError(field: "client_decode", code: "max_items") }
|
||||
for item in self.clientDecode where !["h264-opus", "hevc-opus"].contains(item) { throw ContractValidationError(field: "client_decode", code: "invalid_item") }
|
||||
if Set(self.clientDecode).count != self.clientDecode.count { throw ContractValidationError(field: "client_decode", code: "duplicate_item") }
|
||||
}
|
||||
|
||||
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
|
||||
@@ -2321,7 +2322,8 @@ public struct VersionNegotiation: Codable, Equatable {
|
||||
|
||||
public extension TunnelAdmissionRequest {
|
||||
func deviceAdmissionTranscript() -> Data {
|
||||
let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]
|
||||
var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]
|
||||
fields.append(contentsOf: capabilities.clientDecode)
|
||||
var transcript = "versevdi/tunnel-admission/v1"
|
||||
for field in fields { transcript += "\(field.utf8.count):\(field)" }
|
||||
return Data(transcript.utf8)
|
||||
@@ -2332,10 +2334,13 @@ public extension CapabilityProfile {
|
||||
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
|
||||
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
try selected.validate()
|
||||
var common = selected.clientDecode
|
||||
for profile in profiles.dropFirst() {
|
||||
try profile.validate()
|
||||
if profile != selected { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
common = common.filter { profile.clientDecode.contains($0) }
|
||||
if common.isEmpty { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
}
|
||||
return selected
|
||||
return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ The Data Plane already collects process-wide atomic counters and gauges. Only ac
|
||||
## Decisions
|
||||
|
||||
- Nest the values in required `GatewayTelemetry` so heartbeat telemetry is one strict atomic contract.
|
||||
- Use cumulative counters and microsecond delay totals plus processing samples; consumers can derive rates/averages without losing raw observations.
|
||||
- Use cumulative counters and microsecond delay totals plus one processing sample per complete provider media unit; consumers can derive rates/averages without losing raw observations.
|
||||
- Define queue delay as residence in the bounded provider queue, processing as active recovery/framing/QUIC work excluding queue and scheduler waits, and pacing as scheduler wait only.
|
||||
- Derive measured egress from transmitted-byte deltas over monotonic elapsed time; configured capacity remains registration data.
|
||||
- Keep loss as parts per million and provider state as a bounded enum.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
+7
@@ -13,3 +13,10 @@ Heartbeat telemetry MUST reject unknown fields and MUST NOT include session, rou
|
||||
#### Scenario: Secret or high-cardinality field is attempted
|
||||
- **WHEN** a heartbeat contains an unregistered session, route, endpoint, credential, or payload field
|
||||
- **THEN** strict contract validation rejects it before authenticated transport
|
||||
|
||||
### Requirement: Delay and egress observations have one canonical meaning
|
||||
Queue delay SHALL measure provider-queue residence, processing delay SHALL measure active gateway recovery/framing/QUIC work excluding queue and pacing, and pacing delay SHALL measure scheduler waiting only. Processing samples SHALL count complete provider media units rather than Verse fragments. Measured egress SHALL derive from transmitted-byte deltas over monotonic elapsed time and MUST NOT be copied from configured capacity.
|
||||
|
||||
#### Scenario: One provider unit becomes multiple Verse frames
|
||||
- **WHEN** one complete provider unit waits in the queue, traverses gateway processing, waits for pacing, and fragments into multiple Verse frames
|
||||
- **THEN** each delay total includes only its defined interval and the heartbeat advances processing samples exactly once
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- [x] 1.1 Add bounded GatewayTelemetry to every heartbeat
|
||||
- [x] 1.2 Add Go, Rust, and Swift strict conformance checks
|
||||
- [x] 1.3 Regenerate bindings and prove deterministic output
|
||||
- [x] 1.4 Specify queue, processing, pacing, sample, and measured-egress semantics
|
||||
|
||||
## 2. Consumer Boundary
|
||||
|
||||
|
||||
@@ -7,13 +7,15 @@ The Server resolves an immutable stream-policy version, but RC6 provider work ca
|
||||
**Goals:**
|
||||
|
||||
- Carry only the effective launch settings required by the provider boundary.
|
||||
- Express client decode support as an ordered set of existing registered profiles.
|
||||
- Generate the same ordered registered-profile intersection for every consumer.
|
||||
- Generate identical validation from the canonical schema for all bindings.
|
||||
- Preserve the policy-version identifier for audit correlation.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Publish or mutate RC6.
|
||||
- Add provider-specific capability negotiation to Protocol.
|
||||
- Add a provider-specific token grammar or generic capability framework.
|
||||
- Expose provider work or policy internals to Verse clients.
|
||||
|
||||
## Decisions
|
||||
@@ -22,11 +24,14 @@ The Server resolves an immutable stream-policy version, but RC6 provider work ca
|
||||
- Carry the Server-selected target bitrate rather than all policy bounds because Apollo ANNOUNCE consumes one configured bitrate.
|
||||
- Permit canonical `H264`, `HEVC`, and `AV1` values in the contract. A provider implementation must reject values it cannot honor rather than silently downgrade them.
|
||||
- Carry `audio_enabled` even though the current Apollo path cannot truthfully disable audio; the Data Plane must fail closed for that combination.
|
||||
- Change `client_decode` from one opaque string to a non-empty ordered unique array of registered profile identifiers. Preference belongs to the first peer's order.
|
||||
- Generate `IntersectCapabilityProfiles` from the canonical schema so Protocol, Server, and Data Plane do not maintain separate interpretations.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [New required field breaks RC6 consumers] → Publish only under a separately authorized new immutable version and pin both consumers after empty-cache resolution.
|
||||
- [Provider capabilities differ] → Validate the effective policy against the selected provider before readiness.
|
||||
- [Peers advertise no common registered profile] → Reject admission instead of inventing a combined token or silently downgrading.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ Provider work identifies an immutable stream-policy version but omits the effect
|
||||
## What Changes
|
||||
|
||||
- Add the effective resolution, frame rate, codec, selected bitrate, and audio policy to authenticated provider work.
|
||||
- Represent decode support as an ordered set of registered profiles and generate one canonical intersection operation for consumers.
|
||||
- Require generated Go, Rust, and Swift bindings to validate the same bounded stream-policy contract.
|
||||
- Keep the new contract unpublished until a new immutable Protocol version is separately authorized.
|
||||
|
||||
@@ -12,7 +13,7 @@ Provider work identifies an immutable stream-policy version but omits the effect
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `provider-stream-policy`: Authenticated provider work carries the exact effective stream policy consumed by the provider launch.
|
||||
- `provider-stream-policy`: Authenticated provider work carries the exact effective stream policy consumed by the provider launch, and registered peers negotiate that policy through the shared ordered profile intersection.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
|
||||
@@ -13,3 +13,21 @@ Generated Go, Rust, and Swift bindings MUST reject missing, unknown, out-of-rang
|
||||
#### Scenario: Invalid policy is rejected consistently
|
||||
- **WHEN** provider work contains an unknown codec or a value outside the canonical bounds
|
||||
- **THEN** every generated binding rejects the work before it can reach provider setup
|
||||
|
||||
### Requirement: Decode capabilities use registered ordered profiles
|
||||
`CapabilityProfile.client_decode` SHALL be a non-empty ordered unique set containing only registered `h264-opus` and `hevc-opus` profile identifiers. It MUST NOT encode multiple capabilities in an opaque private token.
|
||||
|
||||
#### Scenario: Independent peer advertises one registered profile
|
||||
- **WHEN** an independent peer advertises one registered decode profile
|
||||
- **THEN** canonical validation accepts that profile without requiring a combined private token
|
||||
|
||||
### Requirement: Consumers share one ordered registered-profile intersection
|
||||
Generated Protocol behavior SHALL select common registered profiles in the first peer's preference order. Provider consumers SHALL separately reject the resulting intersection when it cannot honor the immutable stream policy.
|
||||
|
||||
#### Scenario: Policy-compatible profile overlaps
|
||||
- **WHEN** the gateway advertises HEVC then H.264 and the client advertises only H.264
|
||||
- **THEN** the shared intersection selects `h264-opus`
|
||||
|
||||
#### Scenario: No policy-compatible profile overlaps
|
||||
- **WHEN** peers have no registered common profile
|
||||
- **THEN** the shared intersection rejects admission without inventing a private combined token
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
- [x] 1.1 Add bounded effective stream policy to ProviderSessionWork
|
||||
- [x] 1.2 Add Go, Rust, and Swift conformance coverage
|
||||
- [x] 1.3 Regenerate bindings and prove deterministic output
|
||||
- [x] 1.4 Replace the opaque decode token with an ordered unique set of registered profiles
|
||||
- [x] 1.5 Generate and cross-check canonical ordered registered-profile intersection behavior
|
||||
|
||||
## 2. Consumer Boundary
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ message CapabilityProfile {
|
||||
string media = 3;
|
||||
string audio = 4;
|
||||
string source_rate_control = 5;
|
||||
string client_decode = 6;
|
||||
repeated string client_decode = 6;
|
||||
}
|
||||
|
||||
message GatewayRegistration {
|
||||
|
||||
@@ -376,7 +376,13 @@
|
||||
"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}
|
||||
"client_decode": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
"uniqueItems": true,
|
||||
"items": {"type": "string", "enum": ["h264-opus", "hevc-opus"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"GatewayRegistration": {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package protocol_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -37,7 +38,7 @@ func TestGeneratedDecodersRejectMissingRequiredFieldsAndTrailingValues(t *testin
|
||||
}
|
||||
|
||||
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"}}`
|
||||
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)
|
||||
}
|
||||
@@ -56,7 +57,7 @@ func TestGatewayContractsRejectUnknownVersionsAndFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGatewayRegistrationRejectsInvertedProtocolBounds(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":2,"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"}}`
|
||||
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":2,"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.Fatal("DecodeGatewayRegistration accepted inverted protocol bounds")
|
||||
}
|
||||
@@ -79,24 +80,55 @@ func TestGatewayHeartbeatCarriesBoundedObservedTelemetry(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCapabilityIntersectionRejectsNoOverlap(t *testing.T) {
|
||||
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: "h264-opus"}
|
||||
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || got != first {
|
||||
first := protocol.CapabilityProfile{Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded", SourceRateControl: "server", ClientDecode: []string{"h264-opus"}}
|
||||
if got, err := protocol.IntersectCapabilityProfiles(first, first); err != nil || !reflect.DeepEqual(got, first) {
|
||||
t.Fatalf("IntersectCapabilityProfiles matching profiles = %+v, %v", got, err)
|
||||
}
|
||||
second := first
|
||||
second.ClientDecode = "hevc-opus"
|
||||
second.ClientDecode = []string{"hevc-opus"}
|
||||
if _, err := protocol.IntersectCapabilityProfiles(first, second); err == nil {
|
||||
t.Fatal("IntersectCapabilityProfiles accepted profiles without a common codec profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityIntersectionSelectsRegisteredOrderedProfiles(t *testing.T) {
|
||||
gateway := protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||
SourceRateControl: "server", ClientDecode: []string{"hevc-opus", "h264-opus"},
|
||||
}
|
||||
h264Client := gateway
|
||||
h264Client.ClientDecode = []string{"h264-opus"}
|
||||
selected, err := protocol.IntersectCapabilityProfiles(gateway, h264Client)
|
||||
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"h264-opus"}) {
|
||||
t.Fatalf("H.264 profile intersection = %+v, %v", selected, err)
|
||||
}
|
||||
hevcClient := gateway
|
||||
hevcClient.ClientDecode = []string{"hevc-opus"}
|
||||
selected, err = protocol.IntersectCapabilityProfiles(gateway, hevcClient)
|
||||
if err != nil || !reflect.DeepEqual(selected.ClientDecode, []string{"hevc-opus"}) {
|
||||
t.Fatalf("HEVC profile intersection = %+v, %v", selected, err)
|
||||
}
|
||||
noOverlap := gateway
|
||||
noOverlap.ClientDecode = []string{"h264-opus"}
|
||||
if _, err := protocol.IntersectCapabilityProfiles(noOverlap, hevcClient); err == nil {
|
||||
t.Fatal("intersection accepted registered profiles without overlap")
|
||||
}
|
||||
for _, invalid := range [][]string{{"h264-hevc-opus"}, {"h264-opus", "h264-opus"}} {
|
||||
profile := gateway
|
||||
profile.ClientDecode = invalid
|
||||
if err := profile.Validate(); err == nil {
|
||||
t.Fatalf("CapabilityProfile accepted invalid registered profile set %q", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelAdmissionRequiresDeviceSignature(t *testing.T) {
|
||||
request := protocol.TunnelAdmissionRequest{
|
||||
Version: "1", SessionID: "session-1", GatewayID: "gateway-1", Audience: "versevdi-gateway",
|
||||
Grant: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_", ReconnectSequence: 0,
|
||||
ClientNonce: "0123456789abcdef", Capabilities: protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
|
||||
},
|
||||
}
|
||||
if _, err := protocol.EncodeTunnelAdmissionRequest(request); err == nil {
|
||||
@@ -110,17 +142,17 @@ func TestTunnelAdmissionTranscriptIsDomainSeparatedAndLengthDelimited(t *testing
|
||||
Grant: strings.Repeat("g", 43), ReconnectSequence: 0, ClientNonce: strings.Repeat("n", 16),
|
||||
DeviceSignature: strings.Repeat("s", 86), Capabilities: protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13", Framing: "datagram-v1", Media: "encoded", Audio: "encoded",
|
||||
SourceRateControl: "server", ClientDecode: "h264-opus",
|
||||
SourceRateControl: "server", ClientDecode: []string{"h264-opus"},
|
||||
},
|
||||
}
|
||||
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||
want := "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + strings.Repeat("g", 43) + "1:016:" + strings.Repeat("n", 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
|
||||
if got := string(request.DeviceAdmissionTranscript()); got != want {
|
||||
t.Fatalf("DeviceAdmissionTranscript() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
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"}`
|
||||
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)
|
||||
}
|
||||
|
||||
+44
-11
@@ -149,7 +149,13 @@ def go_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f"\tif len(v.{field}) < {prop['minItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"min_items\"}}) }}")
|
||||
if "maxItems" in prop:
|
||||
lines.append(f"\tif len(v.{field}) > {prop['maxItems']} {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"max_items\"}}) }}")
|
||||
item_ref = ref_name(prop.get("items", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = " || ".join(f'item == "{value}"' for value in items["enum"])
|
||||
lines.append(f"\tfor _, item := range v.{field} {{ if !({allowed}) {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"invalid_item\"}}) }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f"\tfor index, item := range v.{field} {{ for prior := 0; prior < index; prior++ {{ if item == v.{field}[prior] {{ violations = append(violations, FieldViolation{{Field: \"{prop_name}\", Code: \"duplicate_item\"}}) }} }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f"\tfor index := range v.{field} {{ if err := v.{field}[index].Validate(); err != nil {{ violations = append(violations, FieldViolation{{Field: fmt.Sprintf(\"{prop_name}[%d]\", index), Code: \"invalid_item\"}}) }} }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -254,16 +260,23 @@ def generate_go(defs: dict[str, dict[str, Any]], schema_hash: str, version: str,
|
||||
"\tif len(profiles) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tselected := profiles[0]",
|
||||
"\tif err := selected.Validate(); err != nil { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\tcommon := append([]string(nil), selected.ClientDecode...)",
|
||||
"\tfor _, profile := range profiles[1:] {",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile != selected { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tif err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t\tnext := common[:0]",
|
||||
"\t\tfor _, candidate := range common { for _, offered := range profile.ClientDecode { if candidate == offered { next = append(next, candidate); break } } }",
|
||||
"\t\tcommon = next",
|
||||
"\t\tif len(common) == 0 { return CapabilityProfile{}, ErrNoCapabilityOverlap }",
|
||||
"\t}",
|
||||
"\tselected.ClientDecode = common",
|
||||
"\treturn selected, nil",
|
||||
"}",
|
||||
"",
|
||||
])
|
||||
out.extend([
|
||||
"func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}",
|
||||
"\tfields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf(\"%d\", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf(\"%d\", len(v.Capabilities.ClientDecode))}",
|
||||
"\tfields = append(fields, v.Capabilities.ClientDecode...)",
|
||||
"\tvar transcript strings.Builder",
|
||||
"\ttranscript.WriteString(\"versevdi/tunnel-admission/v1\")",
|
||||
"\tfor _, field := range fields { fmt.Fprintf(&transcript, \"%d:%s\", len(field), field) }",
|
||||
@@ -346,7 +359,13 @@ def rust_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if {value}.len() < {prop['minItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"min_items\")); }}")
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.len() > {prop['maxItems']} {{ return Err(ValidationError::new(\"{prop_name}\", \"max_items\")); }}")
|
||||
item_ref = ref_name(prop.get("items", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = " && ".join(f'item != \"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ if {allowed} {{ return Err(ValidationError::new(\"{prop_name}\", \"invalid_item\")); }} }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}for (index, item) in {value}.iter().enumerate() {{ if {value}[..index].contains(item) {{ return Err(ValidationError::new(\"{prop_name}\", \"duplicate_item\")); }} }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value}.iter() {{ item.validate().map_err(|_| ValidationError::new(\"{prop_name}\", \"invalid_item\"))?; }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -440,7 +459,9 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend([
|
||||
" pub fn device_admission_transcript(&self) -> Vec<u8> {",
|
||||
" let reconnect_sequence = self.reconnectSequence.to_string();",
|
||||
" let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];",
|
||||
" let client_decode_count = self.capabilities.clientDecode.len().to_string();",
|
||||
" let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];",
|
||||
" fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));",
|
||||
" let mut transcript = String::from(\"versevdi/tunnel-admission/v1\");",
|
||||
" for field in fields { transcript.push_str(&format!(\"{}:{}\", field.as_bytes().len(), field)); }",
|
||||
" transcript.into_bytes()",
|
||||
@@ -449,11 +470,13 @@ def generate_rust(defs: dict[str, dict[str, Any]], schema_hash: str, compatibili
|
||||
out.extend(["}", ""])
|
||||
out.extend([
|
||||
"pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {",
|
||||
" let selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
||||
" let mut selected = profiles.first().ok_or_else(|| ValidationError::new(\"capabilities\", \"no_overlap\"))?.clone();",
|
||||
" selected.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" for profile in &profiles[1..] {",
|
||||
" profile.validate().map_err(|_| ValidationError::new(\"capabilities\", \"no_overlap\"))?;",
|
||||
" if profile != &selected { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));",
|
||||
" if selected.clientDecode.is_empty() { return Err(ValidationError::new(\"capabilities\", \"no_overlap\")); }",
|
||||
" }",
|
||||
" Ok(selected)",
|
||||
"}",
|
||||
@@ -502,7 +525,13 @@ def swift_validation(definition: dict[str, Any]) -> list[str]:
|
||||
lines.append(f" {prefix}if {value}.count < {prop['minItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"min_items\") }}")
|
||||
if "maxItems" in prop:
|
||||
lines.append(f" {prefix}if {value}.count > {prop['maxItems']} {{ throw ContractValidationError(field: \"{prop_name}\", code: \"max_items\") }}")
|
||||
item_ref = ref_name(prop.get("items", {}))
|
||||
items = prop.get("items", {})
|
||||
if "enum" in items:
|
||||
allowed = ", ".join(f'\"{item}\"' for item in items["enum"])
|
||||
lines.append(f" {prefix}for item in {value} where ![{allowed}].contains(item) {{ throw ContractValidationError(field: \"{prop_name}\", code: \"invalid_item\") }}")
|
||||
if prop.get("uniqueItems") and items.get("type") == "string":
|
||||
lines.append(f" {prefix}if Set({value}).count != {value}.count {{ throw ContractValidationError(field: \"{prop_name}\", code: \"duplicate_item\") }}")
|
||||
item_ref = ref_name(items)
|
||||
if item_ref:
|
||||
lines.append(f" {prefix}for item in {value} {{ try item.validate() }}")
|
||||
reference = ref_name(prop)
|
||||
@@ -583,7 +612,8 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
out.extend([
|
||||
"public extension TunnelAdmissionRequest {",
|
||||
" func deviceAdmissionTranscript() -> Data {",
|
||||
" let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]",
|
||||
" var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]",
|
||||
" fields.append(contentsOf: capabilities.clientDecode)",
|
||||
" var transcript = \"versevdi/tunnel-admission/v1\"",
|
||||
" for field in fields { transcript += \"\\(field.utf8.count):\\(field)\" }",
|
||||
" return Data(transcript.utf8)",
|
||||
@@ -594,11 +624,14 @@ def generate_swift(defs: dict[str, dict[str, Any]], schema_hash: str, compatibil
|
||||
" static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {",
|
||||
" guard let selected = profiles.first else { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" try selected.validate()",
|
||||
" var common = selected.clientDecode",
|
||||
" for profile in profiles.dropFirst() {",
|
||||
" try profile.validate()",
|
||||
" if profile != selected { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" common = common.filter { profile.clientDecode.contains($0) }",
|
||||
" if common.isEmpty { throw ContractValidationError(field: \"capabilities\", code: \"no_overlap\") }",
|
||||
" }",
|
||||
" return selected",
|
||||
" return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)",
|
||||
" }",
|
||||
"}",
|
||||
"",
|
||||
|
||||
@@ -33,7 +33,7 @@ def main() -> int:
|
||||
|
||||
let capability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: "h264-opus"
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["h264-opus"]
|
||||
)
|
||||
let request = try TunnelAdmissionRequest(
|
||||
version: "1", sessionId: "session", gatewayId: "gateway", audience: "audience",
|
||||
@@ -41,19 +41,26 @@ let request = try TunnelAdmissionRequest(
|
||||
clientNonce: String(repeating: "n", count: 16), deviceSignature: String(repeating: "s", count: 86), capabilities: capability
|
||||
)
|
||||
_ = request
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus"
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:" + String(repeating: "g", count: 43) + "1:016:" + String(repeating: "n", count: 16) + "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus"
|
||||
guard String(data: request.deviceAdmissionTranscript(), encoding: .utf8) == transcript else {
|
||||
fatalError("unexpected device admission transcript")
|
||||
}
|
||||
let incompatible = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: "hevc-opus"
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus"]
|
||||
)
|
||||
let gatewayCapability = try CapabilityProfile(
|
||||
transport: "quic-tls13", framing: "datagram-v1", media: "encoded",
|
||||
audio: "encoded", sourceRateControl: "server", clientDecode: ["hevc-opus", "h264-opus"]
|
||||
)
|
||||
do {
|
||||
guard try CapabilityProfile.intersection([capability, capability]) == capability else {
|
||||
fatalError("matching capability profiles did not intersect")
|
||||
}
|
||||
} catch { fatalError("matching capability profiles did not intersect") }
|
||||
guard try CapabilityProfile.intersection([gatewayCapability, capability]).clientDecode == ["h264-opus"] else {
|
||||
fatalError("ordered registered profile intersection changed")
|
||||
}
|
||||
do {
|
||||
_ = try CapabilityProfile.intersection([capability, incompatible])
|
||||
fatalError("profiles without overlap were accepted")
|
||||
@@ -153,7 +160,7 @@ do {
|
||||
fn main() {
|
||||
let capabilities = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), "h264-opus".into(),
|
||||
"encoded".into(), "server".into(), vec!["h264-opus".into()],
|
||||
).unwrap();
|
||||
let request = TunnelAdmissionRequest::new(
|
||||
"1".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -161,7 +168,7 @@ fn main() {
|
||||
).unwrap();
|
||||
let transcript = "versevdi/tunnel-admission/v17:session7:gateway8:audience43:".to_string()
|
||||
+ &"g".repeat(43) + "1:016:" + &"n".repeat(16)
|
||||
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server9:h264-opus";
|
||||
+ "10:quic-tls1311:datagram-v17:encoded7:encoded6:server1:19:h264-opus";
|
||||
assert_eq!(request.device_admission_transcript(), transcript.into_bytes());
|
||||
assert!(TunnelAdmissionRequest::new(
|
||||
"2".into(), "session".into(), "gateway".into(), "audience".into(),
|
||||
@@ -178,8 +185,16 @@ fn main() {
|
||||
assert!(intersect_capability_profiles(&[capabilities.clone(), capabilities.clone()]).is_ok());
|
||||
let incompatible = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), "hevc-opus".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into()],
|
||||
).unwrap();
|
||||
let gateway_capability = CapabilityProfile::new(
|
||||
"quic-tls13".into(), "datagram-v1".into(), "encoded".into(),
|
||||
"encoded".into(), "server".into(), vec!["hevc-opus".into(), "h264-opus".into()],
|
||||
).unwrap();
|
||||
assert_eq!(
|
||||
intersect_capability_profiles(&[gateway_capability, capabilities.clone()]).unwrap().clientDecode(),
|
||||
&vec!["h264-opus".to_string()],
|
||||
);
|
||||
assert!(intersect_capability_profiles(&[capabilities, incompatible]).is_err());
|
||||
assert!(AllocationPolicy::new(
|
||||
100, 50, 25, "standard".into(), "audience".into(), "verse".into(), 1, 60, 300,
|
||||
|
||||
Reference in New Issue
Block a user