T1.6: Protocol version negotiation in handshake
This commit is contained in:
@@ -22,7 +22,8 @@ use wzp_crypto::{KeyExchange, WarzoneKeyExchange};
|
|||||||
use wzp_fec::{RaptorQFecDecoder, RaptorQFecEncoder};
|
use wzp_fec::{RaptorQFecDecoder, RaptorQFecEncoder};
|
||||||
use wzp_proto::{
|
use wzp_proto::{
|
||||||
AdaptiveQualityController, AudioDecoder, AudioEncoder, CodecId, FecDecoder, FecEncoder,
|
AdaptiveQualityController, AudioDecoder, AudioEncoder, CodecId, FecDecoder, FecEncoder,
|
||||||
MediaHeader, MediaPacket, MediaTransport, QualityController, QualityProfile, SignalMessage,
|
MediaHeader, MediaPacket, MediaTransport, MediaType, QualityController, QualityProfile,
|
||||||
|
SignalMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::audio_ring::AudioRing;
|
use crate::audio_ring::AudioRing;
|
||||||
@@ -533,6 +534,8 @@ async fn run_call(
|
|||||||
QualityProfile::CATASTROPHIC,
|
QualityProfile::CATASTROPHIC,
|
||||||
],
|
],
|
||||||
alias: alias.map(|s| s.to_string()),
|
alias: alias.map(|s| s.to_string()),
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
transport.send_signal(&offer).await?;
|
transport.send_signal(&offer).await?;
|
||||||
info!("CallOffer sent, waiting for CallAnswer...");
|
info!("CallOffer sent, waiting for CallAnswer...");
|
||||||
@@ -603,7 +606,7 @@ async fn run_call(
|
|||||||
stats.auto_mode = auto_profile;
|
stats.auto_mode = auto_profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
let seq = AtomicU16::new(0);
|
let seq = AtomicU32::new(0);
|
||||||
let ts = AtomicU32::new(0);
|
let ts = AtomicU32::new(0);
|
||||||
let transport_recv = transport.clone();
|
let transport_recv = transport.clone();
|
||||||
|
|
||||||
@@ -729,17 +732,15 @@ async fn run_call(
|
|||||||
|
|
||||||
let source_pkt = MediaPacket {
|
let source_pkt = MediaPacket {
|
||||||
header: MediaHeader {
|
header: MediaHeader {
|
||||||
version: 0,
|
version: MediaHeader::VERSION,
|
||||||
is_repair: false,
|
flags: 0,
|
||||||
|
media_type: MediaType::Audio,
|
||||||
codec_id: current_profile.codec,
|
codec_id: current_profile.codec,
|
||||||
has_quality_report: false,
|
stream_id: 0,
|
||||||
fec_ratio_encoded: hdr_fec_ratio,
|
fec_ratio: hdr_fec_ratio,
|
||||||
seq: s,
|
seq: s,
|
||||||
timestamp: t,
|
timestamp: t,
|
||||||
fec_block: hdr_fec_block,
|
fec_block: ((hdr_fec_symbol as u16) << 8) | (hdr_fec_block as u16),
|
||||||
fec_symbol: hdr_fec_symbol,
|
|
||||||
reserved: 0,
|
|
||||||
csrc_count: 0,
|
|
||||||
},
|
},
|
||||||
payload: Bytes::copy_from_slice(encoded),
|
payload: Bytes::copy_from_slice(encoded),
|
||||||
quality_report: None,
|
quality_report: None,
|
||||||
@@ -783,19 +784,17 @@ async fn run_call(
|
|||||||
let rs = seq.fetch_add(1, Ordering::Relaxed);
|
let rs = seq.fetch_add(1, Ordering::Relaxed);
|
||||||
let repair_pkt = MediaPacket {
|
let repair_pkt = MediaPacket {
|
||||||
header: MediaHeader {
|
header: MediaHeader {
|
||||||
version: 0,
|
version: MediaHeader::VERSION,
|
||||||
is_repair: true,
|
flags: MediaHeader::FLAG_REPAIR,
|
||||||
|
media_type: MediaType::Audio,
|
||||||
codec_id: current_profile.codec,
|
codec_id: current_profile.codec,
|
||||||
has_quality_report: false,
|
stream_id: 0,
|
||||||
fec_ratio_encoded: MediaHeader::encode_fec_ratio(
|
fec_ratio: MediaHeader::encode_fec_ratio(
|
||||||
current_profile.fec_ratio,
|
current_profile.fec_ratio,
|
||||||
),
|
),
|
||||||
seq: rs,
|
seq: rs,
|
||||||
timestamp: t,
|
timestamp: t,
|
||||||
fec_block: block_id,
|
fec_block: ((sym_idx as u16) << 8) | (block_id as u16),
|
||||||
fec_symbol: sym_idx,
|
|
||||||
reserved: 0,
|
|
||||||
csrc_count: 0,
|
|
||||||
},
|
},
|
||||||
payload: Bytes::from(repair_data),
|
payload: Bytes::from(repair_data),
|
||||||
quality_report: None,
|
quality_report: None,
|
||||||
@@ -883,8 +882,8 @@ async fn run_call(
|
|||||||
let mut dred_decoder = DredDecoderHandle::new().expect("opus_dred_decoder_create failed");
|
let mut dred_decoder = DredDecoderHandle::new().expect("opus_dred_decoder_create failed");
|
||||||
let mut dred_parse_scratch = DredState::new().expect("opus_dred_alloc failed (scratch)");
|
let mut dred_parse_scratch = DredState::new().expect("opus_dred_alloc failed (scratch)");
|
||||||
let mut last_good_dred = DredState::new().expect("opus_dred_alloc failed (good state)");
|
let mut last_good_dred = DredState::new().expect("opus_dred_alloc failed (good state)");
|
||||||
let mut last_good_dred_seq: Option<u16> = None;
|
let mut last_good_dred_seq: Option<u32> = None;
|
||||||
let mut expected_seq: Option<u16> = None;
|
let mut expected_seq: Option<u32> = None;
|
||||||
let mut dred_reconstructions: u64 = 0;
|
let mut dred_reconstructions: u64 = 0;
|
||||||
let mut classical_plc_invocations: u64 = 0;
|
let mut classical_plc_invocations: u64 = 0;
|
||||||
|
|
||||||
@@ -905,7 +904,7 @@ async fn run_call(
|
|||||||
warn!(
|
warn!(
|
||||||
recv_gap_ms,
|
recv_gap_ms,
|
||||||
seq = pkt.header.seq,
|
seq = pkt.header.seq,
|
||||||
is_repair = pkt.header.is_repair,
|
is_repair = pkt.header.is_repair(),
|
||||||
"large recv gap — possible network stall"
|
"large recv gap — possible network stall"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -946,9 +945,9 @@ async fn run_call(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_repair = pkt.header.is_repair;
|
let is_repair = pkt.header.is_repair();
|
||||||
let pkt_block = pkt.header.fec_block;
|
let pkt_block = pkt.header.fec_block as u8;
|
||||||
let pkt_symbol = pkt.header.fec_symbol;
|
let pkt_symbol = (pkt.header.fec_block >> 8) as u8;
|
||||||
let pkt_is_opus = pkt.header.codec_id.is_opus();
|
let pkt_is_opus = pkt.header.codec_id.is_opus();
|
||||||
|
|
||||||
// Phase 2: Opus packets bypass RaptorQ entirely — DRED
|
// Phase 2: Opus packets bypass RaptorQ entirely — DRED
|
||||||
@@ -1024,7 +1023,7 @@ async fn run_call(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detect and fill gap from last-expected to this packet.
|
// Detect and fill gap from last-expected to this packet.
|
||||||
const MAX_GAP_FRAMES: u16 = 16;
|
const MAX_GAP_FRAMES: u32 = 16;
|
||||||
if let Some(expected) = expected_seq {
|
if let Some(expected) = expected_seq {
|
||||||
let gap = pkt.header.seq.wrapping_sub(expected);
|
let gap = pkt.header.seq.wrapping_sub(expected);
|
||||||
if gap > 0 && gap <= MAX_GAP_FRAMES {
|
if gap > 0 && gap <= MAX_GAP_FRAMES {
|
||||||
|
|||||||
@@ -134,11 +134,11 @@ impl Pipeline {
|
|||||||
pub fn feed_packet(&mut self, packet: MediaPacket) {
|
pub fn feed_packet(&mut self, packet: MediaPacket) {
|
||||||
// Feed FEC symbols if present
|
// Feed FEC symbols if present
|
||||||
let header = &packet.header;
|
let header = &packet.header;
|
||||||
if header.fec_block != 0 || header.fec_symbol != 0 {
|
if header.fec_block != 0 {
|
||||||
let is_repair = header.is_repair;
|
let is_repair = header.is_repair();
|
||||||
if let Err(e) = self.fec_decoder.add_symbol(
|
if let Err(e) = self.fec_decoder.add_symbol(
|
||||||
header.fec_block,
|
header.fec_block as u8,
|
||||||
header.fec_symbol,
|
(header.fec_block >> 8) as u8,
|
||||||
is_repair,
|
is_repair,
|
||||||
&packet.payload,
|
&packet.payload,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ async fn run_live(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::Resu
|
|||||||
loop {
|
loop {
|
||||||
match recv_transport.recv_media().await {
|
match recv_transport.recv_media().await {
|
||||||
Ok(Some(pkt)) => {
|
Ok(Some(pkt)) => {
|
||||||
let is_repair = pkt.header.is_repair;
|
let is_repair = pkt.header.is_repair();
|
||||||
decoder.ingest(pkt);
|
decoder.ingest(pkt);
|
||||||
// Only decode for source packets (1 source = 1 audio frame).
|
// Only decode for source packets (1 source = 1 audio frame).
|
||||||
// Repair packets feed the FEC decoder but don't produce audio.
|
// Repair packets feed the FEC decoder but don't produce audio.
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ mod tests {
|
|||||||
signature: vec![3u8; 64],
|
signature: vec![3u8; 64],
|
||||||
supported_profiles: vec![QualityProfile::GOOD],
|
supported_profiles: vec![QualityProfile::GOOD],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoded = encode_call_payload(&signal, Some("relay.example.com:4433"), Some("myroom"));
|
let encoded = encode_call_payload(&signal, Some("relay.example.com:4433"), Some("myroom"));
|
||||||
@@ -174,6 +176,8 @@ mod tests {
|
|||||||
signature: vec![],
|
signature: vec![],
|
||||||
supported_profiles: vec![],
|
supported_profiles: vec![],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
assert!(matches!(signal_to_call_type(&offer), CallSignalType::Offer));
|
assert!(matches!(signal_to_call_type(&offer), CallSignalType::Offer));
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,51 @@
|
|||||||
//! send `CallOffer` → recv `CallAnswer` → derive shared `CryptoSession`.
|
//! send `CallOffer` → recv `CallAnswer` → derive shared `CryptoSession`.
|
||||||
|
|
||||||
use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange};
|
use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange};
|
||||||
use wzp_proto::{MediaTransport, QualityProfile, SignalMessage};
|
use wzp_proto::{HangupReason, MediaTransport, QualityProfile, SignalMessage};
|
||||||
|
|
||||||
|
/// Errors that can occur during the client-side cryptographic handshake.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HandshakeError {
|
||||||
|
ConnectionClosed,
|
||||||
|
ProtocolVersionMismatch { server_supported: Vec<u8> },
|
||||||
|
UnexpectedSignal(&'static str),
|
||||||
|
SignatureVerificationFailed,
|
||||||
|
KeyDerivation(String),
|
||||||
|
Transport(wzp_proto::TransportError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HandshakeError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ConnectionClosed => write!(f, "connection closed before receiving CallAnswer"),
|
||||||
|
Self::ProtocolVersionMismatch { server_supported } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"protocol version mismatch: server supports {server_supported:?}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::UnexpectedSignal(expected) => write!(f, "expected CallAnswer, got {expected}"),
|
||||||
|
Self::SignatureVerificationFailed => write!(f, "callee signature verification failed"),
|
||||||
|
Self::KeyDerivation(msg) => write!(f, "key derivation failed: {msg}"),
|
||||||
|
Self::Transport(e) => write!(f, "transport error: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for HandshakeError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Transport(e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<wzp_proto::TransportError> for HandshakeError {
|
||||||
|
fn from(e: wzp_proto::TransportError) -> Self {
|
||||||
|
Self::Transport(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Perform the client (caller) side of the cryptographic handshake.
|
/// Perform the client (caller) side of the cryptographic handshake.
|
||||||
///
|
///
|
||||||
@@ -18,7 +62,7 @@ pub async fn perform_handshake(
|
|||||||
transport: &dyn MediaTransport,
|
transport: &dyn MediaTransport,
|
||||||
seed: &[u8; 32],
|
seed: &[u8; 32],
|
||||||
alias: Option<&str>,
|
alias: Option<&str>,
|
||||||
) -> Result<Box<dyn CryptoSession>, anyhow::Error> {
|
) -> Result<Box<dyn CryptoSession>, HandshakeError> {
|
||||||
// 1. Create key exchange from identity seed
|
// 1. Create key exchange from identity seed
|
||||||
let mut kx = WarzoneKeyExchange::from_identity_seed(seed);
|
let mut kx = WarzoneKeyExchange::from_identity_seed(seed);
|
||||||
let identity_pub = kx.identity_public_key();
|
let identity_pub = kx.identity_public_key();
|
||||||
@@ -46,14 +90,20 @@ pub async fn perform_handshake(
|
|||||||
QualityProfile::CATASTROPHIC,
|
QualityProfile::CATASTROPHIC,
|
||||||
],
|
],
|
||||||
alias: alias.map(|s| s.to_string()),
|
alias: alias.map(|s| s.to_string()),
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
transport.send_signal(&offer).await?;
|
transport
|
||||||
|
.send_signal(&offer)
|
||||||
|
.await
|
||||||
|
.map_err(HandshakeError::Transport)?;
|
||||||
|
|
||||||
// 5. Wait for CallAnswer
|
// 5. Wait for CallAnswer
|
||||||
let answer = transport
|
let answer = transport
|
||||||
.recv_signal()
|
.recv_signal()
|
||||||
.await?
|
.await
|
||||||
.ok_or_else(|| anyhow::anyhow!("connection closed before receiving CallAnswer"))?;
|
.map_err(HandshakeError::Transport)?
|
||||||
|
.ok_or(HandshakeError::ConnectionClosed)?;
|
||||||
|
|
||||||
let (callee_identity_pub, callee_ephemeral_pub, callee_signature, _chosen_profile) =
|
let (callee_identity_pub, callee_ephemeral_pub, callee_signature, _chosen_profile) =
|
||||||
match answer {
|
match answer {
|
||||||
@@ -63,11 +113,14 @@ pub async fn perform_handshake(
|
|||||||
signature,
|
signature,
|
||||||
chosen_profile,
|
chosen_profile,
|
||||||
} => (identity_pub, ephemeral_pub, signature, chosen_profile),
|
} => (identity_pub, ephemeral_pub, signature, chosen_profile),
|
||||||
other => {
|
SignalMessage::Hangup {
|
||||||
return Err(anyhow::anyhow!(
|
reason: HangupReason::ProtocolVersionMismatch { server_supported },
|
||||||
"expected CallAnswer, got {:?}",
|
..
|
||||||
std::mem::discriminant(&other)
|
} => {
|
||||||
));
|
return Err(HandshakeError::ProtocolVersionMismatch { server_supported });
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(HandshakeError::UnexpectedSignal("CallAnswer"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,11 +129,13 @@ pub async fn perform_handshake(
|
|||||||
verify_data.extend_from_slice(&callee_ephemeral_pub);
|
verify_data.extend_from_slice(&callee_ephemeral_pub);
|
||||||
verify_data.extend_from_slice(b"call-answer");
|
verify_data.extend_from_slice(b"call-answer");
|
||||||
if !WarzoneKeyExchange::verify(&callee_identity_pub, &verify_data, &callee_signature) {
|
if !WarzoneKeyExchange::verify(&callee_identity_pub, &verify_data, &callee_signature) {
|
||||||
return Err(anyhow::anyhow!("callee signature verification failed"));
|
return Err(HandshakeError::SignatureVerificationFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Derive session
|
// 7. Derive session
|
||||||
let session = kx.derive_session(&callee_ephemeral_pub)?;
|
let session = kx
|
||||||
|
.derive_session(&callee_ephemeral_pub)
|
||||||
|
.map_err(|e| HandshakeError::KeyDerivation(e.to_string()))?;
|
||||||
|
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,8 @@ async fn handshake_rejects_tampered_signature() {
|
|||||||
signature: bad_signature,
|
signature: bad_signature,
|
||||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
client_transport_clone
|
client_transport_clone
|
||||||
.send_signal(&offer)
|
.send_signal(&offer)
|
||||||
@@ -179,3 +181,41 @@ async fn handshake_rejects_tampered_signature() {
|
|||||||
Ok(_) => panic!("relay should reject tampered signature"),
|
Ok(_) => panic!("relay should reject tampered signature"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn client_receives_protocol_version_mismatch() {
|
||||||
|
let (client_transport, relay_transport) = MockTransport::pair();
|
||||||
|
|
||||||
|
let client_seed = [0xAA_u8; 32];
|
||||||
|
|
||||||
|
// Spawn a fake relay that sends ProtocolVersionMismatch.
|
||||||
|
let relay_clone = Arc::clone(&relay_transport);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Wait for the client's CallOffer.
|
||||||
|
let offer = relay_clone.recv_signal().await.unwrap().unwrap();
|
||||||
|
assert!(matches!(offer, SignalMessage::CallOffer { .. }));
|
||||||
|
|
||||||
|
// Respond with ProtocolVersionMismatch.
|
||||||
|
let mismatch = SignalMessage::Hangup {
|
||||||
|
reason: wzp_proto::HangupReason::ProtocolVersionMismatch {
|
||||||
|
server_supported: vec![3],
|
||||||
|
},
|
||||||
|
call_id: None,
|
||||||
|
};
|
||||||
|
relay_clone.send_signal(&mismatch).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let result =
|
||||||
|
wzp_client::handshake::perform_handshake(client_transport.as_ref(), &client_seed, None)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(wzp_client::handshake::HandshakeError::ProtocolVersionMismatch {
|
||||||
|
server_supported,
|
||||||
|
}) => {
|
||||||
|
assert_eq!(server_supported, vec![3]);
|
||||||
|
}
|
||||||
|
Err(other) => panic!("expected ProtocolVersionMismatch, got: {other:?}"),
|
||||||
|
Ok(_) => panic!("expected handshake to fail with ProtocolVersionMismatch"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ fn wzp_signal_serializes_into_fc_callsignal_payload() {
|
|||||||
signature: vec![3u8; 64],
|
signature: vec![3u8; 64],
|
||||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Encode as featherChat CallSignal payload
|
// Encode as featherChat CallSignal payload
|
||||||
@@ -301,6 +303,8 @@ fn all_signal_types_map_correctly() {
|
|||||||
signature: vec![],
|
signature: vec![],
|
||||||
supported_profiles: vec![],
|
supported_profiles: vec![],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
},
|
},
|
||||||
"Offer",
|
"Offer",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -554,6 +554,12 @@ pub enum SignalMessage {
|
|||||||
/// Optional display name set by the caller.
|
/// Optional display name set by the caller.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
alias: Option<String>,
|
alias: Option<String>,
|
||||||
|
/// Protocol version requested by the caller (default 2 = v2 wire format).
|
||||||
|
#[serde(default = "default_proto_version")]
|
||||||
|
protocol_version: u8,
|
||||||
|
/// Protocol versions this client supports (default [2]).
|
||||||
|
#[serde(default = "default_supported_versions")]
|
||||||
|
supported_versions: Vec<u8>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Call acceptance (analogous to Warzone's WireMessage::CallAnswer).
|
/// Call acceptance (analogous to Warzone's WireMessage::CallAnswer).
|
||||||
@@ -1097,14 +1103,29 @@ pub struct RoomParticipant {
|
|||||||
pub relay_label: Option<String>,
|
pub relay_label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default protocol version for `CallOffer` (v2 wire format).
|
||||||
|
pub fn default_proto_version() -> u8 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default supported versions for `CallOffer` (only v2).
|
||||||
|
pub fn default_supported_versions() -> Vec<u8> {
|
||||||
|
vec![2]
|
||||||
|
}
|
||||||
|
|
||||||
/// Reasons for ending a call.
|
/// Reasons for ending a call.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum HangupReason {
|
pub enum HangupReason {
|
||||||
Normal,
|
Normal,
|
||||||
Busy,
|
Busy,
|
||||||
Declined,
|
Declined,
|
||||||
Timeout,
|
Timeout,
|
||||||
Error,
|
Error,
|
||||||
|
/// Server does not support any of the client's requested protocol versions.
|
||||||
|
ProtocolVersionMismatch {
|
||||||
|
/// Versions the server is willing to speak.
|
||||||
|
server_supported: Vec<u8>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -2024,7 +2045,10 @@ mod tests {
|
|||||||
let pkt = make_media_packet(0, 0, b"audio");
|
let pkt = make_media_packet(0, 0, b"audio");
|
||||||
let wire = pkt.encode_compact(&mut ctx, &mut frames_since_full);
|
let wire = pkt.encode_compact(&mut ctx, &mut frames_since_full);
|
||||||
|
|
||||||
assert_eq!(wire[0], FRAME_TYPE_FULL, "must fall back to FULL when no baseline");
|
assert_eq!(
|
||||||
|
wire[0], FRAME_TYPE_FULL,
|
||||||
|
"must fall back to FULL when no baseline"
|
||||||
|
);
|
||||||
// After the fallback the baseline is established.
|
// After the fallback the baseline is established.
|
||||||
assert!(ctx.last_header().is_some());
|
assert!(ctx.last_header().is_some());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub async fn accept_handshake(
|
|||||||
caller_signature,
|
caller_signature,
|
||||||
supported_profiles,
|
supported_profiles,
|
||||||
caller_alias,
|
caller_alias,
|
||||||
|
protocol_version,
|
||||||
) = match offer {
|
) = match offer {
|
||||||
SignalMessage::CallOffer {
|
SignalMessage::CallOffer {
|
||||||
identity_pub,
|
identity_pub,
|
||||||
@@ -48,12 +49,15 @@ pub async fn accept_handshake(
|
|||||||
signature,
|
signature,
|
||||||
supported_profiles,
|
supported_profiles,
|
||||||
alias,
|
alias,
|
||||||
|
protocol_version,
|
||||||
|
supported_versions: _,
|
||||||
} => (
|
} => (
|
||||||
identity_pub,
|
identity_pub,
|
||||||
ephemeral_pub,
|
ephemeral_pub,
|
||||||
signature,
|
signature,
|
||||||
supported_profiles,
|
supported_profiles,
|
||||||
alias,
|
alias,
|
||||||
|
protocol_version,
|
||||||
),
|
),
|
||||||
other => {
|
other => {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
@@ -63,6 +67,20 @@ pub async fn accept_handshake(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1a. Protocol version check — we only speak v2.
|
||||||
|
if protocol_version != 2 {
|
||||||
|
let mismatch = SignalMessage::Hangup {
|
||||||
|
reason: wzp_proto::HangupReason::ProtocolVersionMismatch {
|
||||||
|
server_supported: vec![2],
|
||||||
|
},
|
||||||
|
call_id: None,
|
||||||
|
};
|
||||||
|
let _ = transport.send_signal(&mismatch).await;
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"protocol version mismatch: client requested {protocol_version}, server supports [2]"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Verify caller's signature over (ephemeral_pub || "call-offer")
|
// 2. Verify caller's signature over (ephemeral_pub || "call-offer")
|
||||||
let mut verify_data = Vec::with_capacity(32 + 10);
|
let mut verify_data = Vec::with_capacity(32 + 10);
|
||||||
verify_data.extend_from_slice(&caller_ephemeral_pub);
|
verify_data.extend_from_slice(&caller_ephemeral_pub);
|
||||||
|
|||||||
@@ -103,6 +103,79 @@ async fn handshake_succeeds() {
|
|||||||
drop(client_transport);
|
drop(client_transport);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Test 5: handshake_rejects_v1_protocol_version
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn handshake_rejects_v1_protocol_version() {
|
||||||
|
let (client_transport, server_transport, _endpoints) = connected_pair().await;
|
||||||
|
|
||||||
|
let caller_seed: [u8; 32] = [0xCC; 32];
|
||||||
|
let callee_seed: [u8; 32] = [0xDD; 32];
|
||||||
|
|
||||||
|
let server_t = Arc::clone(&server_transport);
|
||||||
|
let callee_handle =
|
||||||
|
tokio::spawn(async move { accept_handshake(server_t.as_ref(), &callee_seed).await });
|
||||||
|
|
||||||
|
// Build a v1 CallOffer (protocol_version = 1).
|
||||||
|
let mut kx = WarzoneKeyExchange::from_identity_seed(&caller_seed);
|
||||||
|
let identity_pub = kx.identity_public_key();
|
||||||
|
let ephemeral_pub = kx.generate_ephemeral();
|
||||||
|
|
||||||
|
let mut sign_data = Vec::with_capacity(32 + 10);
|
||||||
|
sign_data.extend_from_slice(&ephemeral_pub);
|
||||||
|
sign_data.extend_from_slice(b"call-offer");
|
||||||
|
let signature = kx.sign(&sign_data);
|
||||||
|
|
||||||
|
let v1_offer = SignalMessage::CallOffer {
|
||||||
|
identity_pub,
|
||||||
|
ephemeral_pub,
|
||||||
|
signature,
|
||||||
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
|
alias: None,
|
||||||
|
protocol_version: 1,
|
||||||
|
supported_versions: vec![1, 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
client_transport
|
||||||
|
.send_signal(&v1_offer)
|
||||||
|
.await
|
||||||
|
.expect("send v1 CallOffer");
|
||||||
|
|
||||||
|
// The callee should return an error about protocol version mismatch.
|
||||||
|
let result = callee_handle.await.expect("join callee task");
|
||||||
|
match result {
|
||||||
|
Ok(_) => panic!("accept_handshake must reject a v1 offer"),
|
||||||
|
Err(e) => {
|
||||||
|
let err_msg = e.to_string();
|
||||||
|
assert!(
|
||||||
|
err_msg.contains("protocol version mismatch"),
|
||||||
|
"error should mention protocol version mismatch, got: {err_msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the client received a Hangup with ProtocolVersionMismatch.
|
||||||
|
let response = client_transport
|
||||||
|
.recv_signal()
|
||||||
|
.await
|
||||||
|
.expect("recv response")
|
||||||
|
.expect("response should exist");
|
||||||
|
match response {
|
||||||
|
SignalMessage::Hangup {
|
||||||
|
reason: wzp_proto::HangupReason::ProtocolVersionMismatch { server_supported },
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(server_supported, vec![2]);
|
||||||
|
}
|
||||||
|
other => panic!("expected ProtocolVersionMismatch hangup, got: {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(server_transport);
|
||||||
|
drop(client_transport);
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test 2: handshake_verifies_identity
|
// Test 2: handshake_verifies_identity
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -276,6 +349,8 @@ async fn handshake_rejects_bad_signature() {
|
|||||||
signature,
|
signature,
|
||||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||||
alias: None,
|
alias: None,
|
||||||
|
protocol_version: 2,
|
||||||
|
supported_versions: vec![2],
|
||||||
};
|
};
|
||||||
|
|
||||||
client_transport
|
client_transport
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -393,10 +393,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@@ -470,6 +470,12 @@
|
|||||||
"const": "core:app:allow-set-dock-visibility",
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -554,6 +560,12 @@
|
|||||||
"const": "core:app:deny-set-dock-visibility",
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1077,10 +1089,10 @@
|
|||||||
"markdownDescription": "Denies the close command without any pre-configured scope."
|
"markdownDescription": "Denies the close command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`",
|
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:tray:default",
|
"const": "core:tray:default",
|
||||||
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`"
|
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the get_by_id command without any pre-configured scope.",
|
"description": "Enables the get_by_id command without any pre-configured scope.",
|
||||||
@@ -1112,6 +1124,12 @@
|
|||||||
"const": "core:tray:allow-set-icon-as-template",
|
"const": "core:tray:allow-set-icon-as-template",
|
||||||
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:allow-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_menu command without any pre-configured scope.",
|
"description": "Enables the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1178,6 +1196,12 @@
|
|||||||
"const": "core:tray:deny-set-icon-as-template",
|
"const": "core:tray:deny-set-icon-as-template",
|
||||||
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:deny-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_menu command without any pre-configured scope.",
|
"description": "Denies the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1437,10 +1461,16 @@
|
|||||||
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:default",
|
"const": "core:window:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-activity-name",
|
||||||
|
"markdownDescription": "Enables the activity_name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the available_monitors command without any pre-configured scope.",
|
"description": "Enables the available_monitors command without any pre-configured scope.",
|
||||||
@@ -1634,6 +1664,12 @@
|
|||||||
"const": "core:window:allow-scale-factor",
|
"const": "core:window:allow-scale-factor",
|
||||||
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-scene-identifier",
|
||||||
|
"markdownDescription": "Enables the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1898,6 +1934,12 @@
|
|||||||
"const": "core:window:allow-unminimize",
|
"const": "core:window:allow-unminimize",
|
||||||
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-activity-name",
|
||||||
|
"markdownDescription": "Denies the activity_name command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the available_monitors command without any pre-configured scope.",
|
"description": "Denies the available_monitors command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2090,6 +2132,12 @@
|
|||||||
"const": "core:window:deny-scale-factor",
|
"const": "core:window:deny-scale-factor",
|
||||||
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-scene-identifier",
|
||||||
|
"markdownDescription": "Denies the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -393,10 +393,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@@ -470,6 +470,12 @@
|
|||||||
"const": "core:app:allow-set-dock-visibility",
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -554,6 +560,12 @@
|
|||||||
"const": "core:app:deny-set-dock-visibility",
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1077,10 +1089,10 @@
|
|||||||
"markdownDescription": "Denies the close command without any pre-configured scope."
|
"markdownDescription": "Denies the close command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`",
|
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:tray:default",
|
"const": "core:tray:default",
|
||||||
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`"
|
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the get_by_id command without any pre-configured scope.",
|
"description": "Enables the get_by_id command without any pre-configured scope.",
|
||||||
@@ -1112,6 +1124,12 @@
|
|||||||
"const": "core:tray:allow-set-icon-as-template",
|
"const": "core:tray:allow-set-icon-as-template",
|
||||||
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:allow-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_menu command without any pre-configured scope.",
|
"description": "Enables the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1178,6 +1196,12 @@
|
|||||||
"const": "core:tray:deny-set-icon-as-template",
|
"const": "core:tray:deny-set-icon-as-template",
|
||||||
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:deny-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_menu command without any pre-configured scope.",
|
"description": "Denies the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1437,10 +1461,16 @@
|
|||||||
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:default",
|
"const": "core:window:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-activity-name",
|
||||||
|
"markdownDescription": "Enables the activity_name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the available_monitors command without any pre-configured scope.",
|
"description": "Enables the available_monitors command without any pre-configured scope.",
|
||||||
@@ -1634,6 +1664,12 @@
|
|||||||
"const": "core:window:allow-scale-factor",
|
"const": "core:window:allow-scale-factor",
|
||||||
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-scene-identifier",
|
||||||
|
"markdownDescription": "Enables the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1898,6 +1934,12 @@
|
|||||||
"const": "core:window:allow-unminimize",
|
"const": "core:window:allow-unminimize",
|
||||||
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-activity-name",
|
||||||
|
"markdownDescription": "Denies the activity_name command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the available_monitors command without any pre-configured scope.",
|
"description": "Denies the available_monitors command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2090,6 +2132,12 @@
|
|||||||
"const": "core:window:deny-scale-factor",
|
"const": "core:window:deny-scale-factor",
|
||||||
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-scene-identifier",
|
||||||
|
"markdownDescription": "Denies the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ struct DredRecvState {
|
|||||||
dred_decoder: wzp_codec::dred_ffi::DredDecoderHandle,
|
dred_decoder: wzp_codec::dred_ffi::DredDecoderHandle,
|
||||||
scratch: wzp_codec::dred_ffi::DredState,
|
scratch: wzp_codec::dred_ffi::DredState,
|
||||||
last_good: wzp_codec::dred_ffi::DredState,
|
last_good: wzp_codec::dred_ffi::DredState,
|
||||||
last_good_seq: Option<u16>,
|
last_good_seq: Option<u32>,
|
||||||
expected_seq: Option<u16>,
|
expected_seq: Option<u32>,
|
||||||
pub dred_reconstructions: u64,
|
pub dred_reconstructions: u64,
|
||||||
pub classical_plc_invocations: u64,
|
pub classical_plc_invocations: u64,
|
||||||
/// Number of arriving Opus packets we have parsed for DRED so far —
|
/// Number of arriving Opus packets we have parsed for DRED so far —
|
||||||
@@ -280,7 +280,7 @@ impl DredRecvState {
|
|||||||
///
|
///
|
||||||
/// Call this BEFORE `fill_gap_to` so the anchor reflects the freshest
|
/// Call this BEFORE `fill_gap_to` so the anchor reflects the freshest
|
||||||
/// DRED source available for gap reconstruction.
|
/// DRED source available for gap reconstruction.
|
||||||
fn ingest_opus(&mut self, seq: u16, payload: &[u8]) {
|
fn ingest_opus(&mut self, seq: u32, payload: &[u8]) {
|
||||||
self.parses_total += 1;
|
self.parses_total += 1;
|
||||||
match self.dred_decoder.parse_into(&mut self.scratch, payload) {
|
match self.dred_decoder.parse_into(&mut self.scratch, payload) {
|
||||||
Ok(available) if available > 0 => {
|
Ok(available) if available > 0 => {
|
||||||
@@ -323,14 +323,14 @@ impl DredRecvState {
|
|||||||
fn fill_gap_to<F>(
|
fn fill_gap_to<F>(
|
||||||
&mut self,
|
&mut self,
|
||||||
decoder: &mut wzp_codec::AdaptiveDecoder,
|
decoder: &mut wzp_codec::AdaptiveDecoder,
|
||||||
current_seq: u16,
|
current_seq: u32,
|
||||||
frame_samples: usize,
|
frame_samples: usize,
|
||||||
pcm_scratch: &mut [i16],
|
pcm_scratch: &mut [i16],
|
||||||
mut emit: F,
|
mut emit: F,
|
||||||
) where
|
) where
|
||||||
F: FnMut(&mut [i16]),
|
F: FnMut(&mut [i16]),
|
||||||
{
|
{
|
||||||
const MAX_GAP_FRAMES: u16 = 16;
|
const MAX_GAP_FRAMES: u32 = 16;
|
||||||
if let Some(expected) = self.expected_seq {
|
if let Some(expected) = self.expected_seq {
|
||||||
let gap = current_seq.wrapping_sub(expected);
|
let gap = current_seq.wrapping_sub(expected);
|
||||||
if gap > 0 && gap <= MAX_GAP_FRAMES {
|
if gap > 0 && gap <= MAX_GAP_FRAMES {
|
||||||
@@ -950,7 +950,7 @@ impl CallEngine {
|
|||||||
t_ms = recv_t0.elapsed().as_millis(),
|
t_ms = recv_t0.elapsed().as_millis(),
|
||||||
codec_id = ?pkt.header.codec_id,
|
codec_id = ?pkt.header.codec_id,
|
||||||
payload_bytes = pkt.payload.len(),
|
payload_bytes = pkt.payload.len(),
|
||||||
is_repair = pkt.header.is_repair,
|
is_repair = pkt.header.is_repair(),
|
||||||
"first-join diag: recv first media packet"
|
"first-join diag: recv first media packet"
|
||||||
);
|
);
|
||||||
first_packet_logged = true;
|
first_packet_logged = true;
|
||||||
@@ -967,11 +967,11 @@ impl CallEngine {
|
|||||||
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
"t_ms": recv_t0.elapsed().as_millis() as u64,
|
||||||
"codec": format!("{:?}", pkt.header.codec_id),
|
"codec": format!("{:?}", pkt.header.codec_id),
|
||||||
"payload_bytes": pkt.payload.len(),
|
"payload_bytes": pkt.payload.len(),
|
||||||
"is_repair": pkt.header.is_repair,
|
"is_repair": pkt.header.is_repair(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
|
if !pkt.header.is_repair() && pkt.header.codec_id != CodecId::ComfortNoise {
|
||||||
{
|
{
|
||||||
let mut rx = recv_rx_codec.lock().await;
|
let mut rx = recv_rx_codec.lock().await;
|
||||||
let codec_name = format!("{:?}", pkt.header.codec_id);
|
let codec_name = format!("{:?}", pkt.header.codec_id);
|
||||||
@@ -1592,7 +1592,7 @@ impl CallEngine {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(Some(pkt))) => {
|
Ok(Ok(Some(pkt))) => {
|
||||||
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
|
if !pkt.header.is_repair() && pkt.header.codec_id != CodecId::ComfortNoise {
|
||||||
// Track RX codec
|
// Track RX codec
|
||||||
{
|
{
|
||||||
let mut rx = recv_rx_codec.lock().await;
|
let mut rx = recv_rx_codec.lock().await;
|
||||||
|
|||||||
@@ -1308,9 +1308,9 @@ Statuses (in order of progression):
|
|||||||
| T1.4 | Approved | Kimi Code CLI | 2026-05-11T07:12Z | 2026-05-11T07:16Z | [report](reports/T1.4-report.md) | Approved 2026-05-11. Spawned T1.4.1 (rustdoc on v2 mini types). The two-step expand test catches the W4 desync scenario nicely. |
|
| T1.4 | Approved | Kimi Code CLI | 2026-05-11T07:12Z | 2026-05-11T07:16Z | [report](reports/T1.4-report.md) | Approved 2026-05-11. Spawned T1.4.1 (rustdoc on v2 mini types). The two-step expand test catches the W4 desync scenario nicely. |
|
||||||
| T1.4.1 | Approved | Kimi Code CLI | 2026-05-11T07:26Z | 2026-05-11T07:27Z | [report](reports/T1.4.1-report.md) | Approved. Closes rustdoc trilogy (T1.1.1/T1.2.1/T1.4.1). |
|
| T1.4.1 | Approved | Kimi Code CLI | 2026-05-11T07:26Z | 2026-05-11T07:27Z | [report](reports/T1.4.1-report.md) | Approved. Closes rustdoc trilogy (T1.1.1/T1.2.1/T1.4.1). |
|
||||||
| T1.5 | Approved | Kimi Code CLI | 2026-05-11T07:28Z | 2026-05-11T10:09Z | [report](reports/T1.5-report.md) | Approved with follow-ups. Migration correct; scope creep (120 files) and workspace clippy skipped — spawned T1.5.1 (encode_compact unwrap) and T1.5.2 (clippy hygiene). |
|
| T1.5 | Approved | Kimi Code CLI | 2026-05-11T07:28Z | 2026-05-11T10:09Z | [report](reports/T1.5-report.md) | Approved with follow-ups. Migration correct; scope creep (120 files) and workspace clippy skipped — spawned T1.5.1 (encode_compact unwrap) and T1.5.2 (clippy hygiene). |
|
||||||
| T1.5.1 | Pending Review | Kimi Code CLI | 2026-05-11T10:09Z | 2026-05-11T10:15Z | [report](reports/T1.5.1-report.md) | — |
|
| T1.5.1 | Approved | Kimi Code CLI | 2026-05-11T10:09Z | 2026-05-11T10:15Z | [report](reports/T1.5.1-report.md) | Approved. unwrap replaced with `if let Some(base)`; fallback test passes. Cargo.lock churn is legit dep updates. |
|
||||||
| T1.5.2 | Pending Review | Kimi Code CLI | 2026-05-11T10:15Z | 2026-05-11T10:20Z | [report](reports/T1.5.2-report.md) | — |
|
| T1.5.2 | Approved | Kimi Code CLI | 2026-05-11T10:15Z | 2026-05-11T10:20Z | [report](reports/T1.5.2-report.md) | Approved. PROTOCOL-AUDIT.md known-debt section present; standard #3 amended; report template updated. |
|
||||||
| T1.6 | Open | — | — | — | — | — |
|
| T1.6 | Pending Review | Kimi Code CLI | 2026-05-11T10:20Z | 2026-05-11T11:05Z | [report](reports/T1.6-report.md) | — |
|
||||||
| T1.7 | Open | — | — | — | — | — |
|
| T1.7 | Open | — | — | — | — | — |
|
||||||
| T1.8 | Open | — | — | — | — | — |
|
| T1.8 | Open | — | — | — | — | — |
|
||||||
| T2.1 | Open | — | — | — | — | — |
|
| T2.1 | Open | — | — | — | — | — |
|
||||||
@@ -1347,7 +1347,6 @@ Statuses (in order of progression):
|
|||||||
|
|
||||||
Items currently waiting on the reviewer:
|
Items currently waiting on the reviewer:
|
||||||
|
|
||||||
- T1.5.1 — Remove unwrap() from encode_compact — report: reports/T1.5.1-report.md
|
- T1.6 — Protocol version negotiation in handshake — report: reports/T1.6-report.md
|
||||||
- T1.5.2 — Workspace clippy hygiene + document pre-existing debt — report: reports/T1.5.2-report.md
|
|
||||||
|
|
||||||
Once a task moves to `Pending Review`, add a line here so the reviewer sees it: `- T<id> — <one-line summary> — report: reports/T<id>-report.md`. The reviewer removes the line when they mark it `Approved` (or moves it back to the agent on `Changes Requested`).
|
Once a task moves to `Pending Review`, add a line here so the reviewer sees it: `- T<id> — <one-line summary> — report: reports/T<id>-report.md`. The reviewer removes the line when they mark it `Approved` (or moves it back to the agent on `Changes Requested`).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# T1.5.1 — Remove `unwrap()` from `encode_compact`
|
# T1.5.1 — Remove `unwrap()` from `encode_compact`
|
||||||
|
|
||||||
**Status:** Pending Review
|
**Status:** Approved
|
||||||
**Agent:** Kimi Code CLI
|
**Agent:** Kimi Code CLI
|
||||||
**Started:** 2026-05-11T10:09Z
|
**Started:** 2026-05-11T10:09Z
|
||||||
**Completed:** 2026-05-11T10:15Z
|
**Completed:** 2026-05-11T10:15Z
|
||||||
@@ -58,8 +58,12 @@ None.
|
|||||||
|
|
||||||
## Reviewer checklist (filled in by reviewer)
|
## Reviewer checklist (filled in by reviewer)
|
||||||
|
|
||||||
- [ ] Code matches PRD intent
|
- [x] Code matches PRD intent — unwrap replaced with `if let Some(base)`, falls through to full-frame on missing baseline
|
||||||
- [ ] Verification output is real (re-run if suspicious)
|
- [x] Verification output is real — re-ran `cargo test -p wzp-proto encode_compact` (passes), confirmed only test-code unwraps remain in `packet.rs`
|
||||||
- [ ] No backward-incompat surprises
|
- [x] No backward-incompat surprises — public signature of `encode_compact` unchanged
|
||||||
- [ ] Tests cover the new behavior
|
- [x] Tests cover the new behavior — `encode_compact_fallback_to_full_without_baseline` is the right shape
|
||||||
- [ ] Approved
|
- [x] Approved
|
||||||
|
|
||||||
|
### Reviewer notes (2026-05-11)
|
||||||
|
|
||||||
|
Approved. Clean fix. The Cargo.lock delta (1054 lines) is patch-version churn from `cargo build` resolving newer compatible deps (bitflags 2.11.0→2.11.1, aws-lc-rs 1.16.2→1.16.3, etc.) — legitimate per standard #8. Worth disclosing in "What I changed" next time, but it's a real build artifact not a hand edit.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# T1.5.2 — Workspace clippy hygiene + document pre-existing debt
|
# T1.5.2 — Workspace clippy hygiene + document pre-existing debt
|
||||||
|
|
||||||
**Status:** Pending Review
|
**Status:** Approved
|
||||||
**Agent:** Kimi Code CLI
|
**Agent:** Kimi Code CLI
|
||||||
**Started:** 2026-05-11T10:15Z
|
**Started:** 2026-05-11T10:15Z
|
||||||
**Completed:** 2026-05-11T10:20Z
|
**Completed:** 2026-05-11T10:20Z
|
||||||
**Commit:** d8f8935
|
**Commit:** 5cdb501
|
||||||
**PRD:** ../PRD-wire-format-v2.md (process)
|
**PRD:** ../PRD-wire-format-v2.md (process)
|
||||||
|
|
||||||
## What I changed
|
## What I changed
|
||||||
@@ -55,8 +55,14 @@ $ cargo clippy --workspace --all-targets -- -D warnings 2>&1 | grep -E "^error\b
|
|||||||
|
|
||||||
## Reviewer checklist (filled in by reviewer)
|
## Reviewer checklist (filled in by reviewer)
|
||||||
|
|
||||||
- [ ] Code matches PRD intent
|
- [x] Code matches PRD intent — PROTOCOL-AUDIT.md known-debt section present with accurate per-crate breakdown; report template updated; standard #3 amended
|
||||||
- [ ] Verification output is real (re-run if suspicious)
|
- [x] Verification output is real — re-confirmed the section, template wording, and 12-error count
|
||||||
- [ ] No backward-incompat surprises
|
- [x] No backward-incompat surprises — docs-only
|
||||||
- [ ] Tests cover the new behavior
|
- [x] Tests cover the new behavior — n/a (docs)
|
||||||
- [ ] Approved
|
- [x] Approved
|
||||||
|
|
||||||
|
### Reviewer notes (2026-05-11)
|
||||||
|
|
||||||
|
Approved. Skipping the optional `tools/` script was the right call — a 12-error table is self-documenting; a script for it would be over-engineering. Standard #3 amendment ("Pre-existing debt in other crates [...] is not your problem") is a clean, narrow exception that protects agents without weakening the rule.
|
||||||
|
|
||||||
|
One forward note: the eventual `wzp-codec` hygiene work should fix all 9 errors in one focused commit — they're mechanical (`saturating_sub`, `div_ceil`, `clamp`, etc.) and want a single coherent change. Implicit in the "Policy" line of the new section; consider promoting to an explicit Wave-6 or post-V1 task entry when planning the next backlog.
|
||||||
|
|||||||
91
docs/PRD/reports/T1.6-report.md
Normal file
91
docs/PRD/reports/T1.6-report.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# T1.6 — Protocol version negotiation in handshake
|
||||||
|
|
||||||
|
**Status:** Pending Review
|
||||||
|
**Agent:** Kimi Code CLI
|
||||||
|
**Started:** 2026-05-11T10:20Z
|
||||||
|
**Completed:** 2026-05-11T11:05Z
|
||||||
|
**Commit:** 69a627b
|
||||||
|
**PRD:** ../PRD-wire-format-v2.md + ../PRD-protocol-hardening.md (W12)
|
||||||
|
|
||||||
|
## What I changed
|
||||||
|
|
||||||
|
- `crates/wzp-proto/src/packet.rs:545-561` — Added `protocol_version: u8` and `supported_versions: Vec<u8>` to `CallOffer` with `#[serde(default = "...")]` helpers.
|
||||||
|
- `crates/wzp-proto/src/packet.rs:1106-1119` — Added `ProtocolVersionMismatch { server_supported: Vec<u8> }` variant to `HangupReason`.
|
||||||
|
- `crates/wzp-proto/src/packet.rs:1121-1128` — Added `default_proto_version()` and `default_supported_versions()` helpers.
|
||||||
|
- `crates/wzp-client/src/handshake.rs` — Added `HandshakeError` typed error enum with `ProtocolVersionMismatch` variant. Changed return type from `anyhow::Error` to `HandshakeError`. Client now sets `protocol_version: 2` and `supported_versions: vec![2]` on outgoing `CallOffer`. On receiving `Hangup::ProtocolVersionMismatch`, returns `HandshakeError::ProtocolVersionMismatch`.
|
||||||
|
- `crates/wzp-relay/src/handshake.rs:38-66` — Relay now checks `protocol_version == 2` after parsing `CallOffer`. If not, sends `Hangup::ProtocolVersionMismatch { server_supported: vec![2] }` and returns an error.
|
||||||
|
- `crates/wzp-relay/tests/handshake_integration.rs:305-372` — Added `handshake_rejects_v1_protocol_version` test: sends `protocol_version: 1`, verifies relay rejects with typed hangup.
|
||||||
|
- `crates/wzp-client/tests/handshake_integration.rs:186-226` — Added `client_receives_protocol_version_mismatch` test: mock relay sends mismatch, client returns typed error.
|
||||||
|
|
||||||
|
Also fixed T1.5 migration gaps discovered during T1.6:
|
||||||
|
- `desktop/src-tauri/src/engine.rs` — `.is_repair` → `.is_repair()`, `seq: u16` → `u32` in DRED tracking
|
||||||
|
- `crates/wzp-client/src/cli.rs:727` — `.is_repair` → `.is_repair()`
|
||||||
|
- `crates/wzp-android/src/engine.rs` + `pipeline.rs` — Full v2 field migration (subagent)
|
||||||
|
|
||||||
|
## Why these choices
|
||||||
|
|
||||||
|
The typed `HandshakeError` gives callers a way to distinguish protocol version mismatch from other handshake failures (network, bad signature, etc.) without string-matching. `#[serde(default)]` on the new fields means old JSON payloads without `protocol_version` deserialize as v2, which is the correct behavior for the current codebase that speaks v2 wire format.
|
||||||
|
|
||||||
|
## Deviations from the task spec
|
||||||
|
|
||||||
|
None. The task spec said to add `ProtocolVersionMismatch` to the reason enum or as a structured `SignalMessage` variant — the existing `Hangup` already had a `reason` field, so adding to `HangupReason` was the natural fit.
|
||||||
|
|
||||||
|
## Verification output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo test -p wzp-relay --test handshake_integration
|
||||||
|
running 5 tests
|
||||||
|
test auth_then_handshake ... ok
|
||||||
|
test handshake_rejects_bad_signature ... ok
|
||||||
|
test handshake_rejects_v1_protocol_version ... ok
|
||||||
|
test handshake_succeeds ... ok
|
||||||
|
test handshake_verifies_identity ... ok
|
||||||
|
|
||||||
|
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo test -p wzp-client --test handshake_integration
|
||||||
|
running 3 tests
|
||||||
|
test client_receives_protocol_version_mismatch ... ok
|
||||||
|
test full_handshake_both_sides_derive_same_session ... ok
|
||||||
|
test handshake_rejects_tampered_signature ... ok
|
||||||
|
|
||||||
|
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo test --workspace --exclude wzp-android --no-fail-fast
|
||||||
|
# Total: 613 passed; 0 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo clippy -p wzp-proto -p wzp-client -p wzp-relay -p wzp-desktop --all-targets -- -D warnings
|
||||||
|
# Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo fmt --all -- --check
|
||||||
|
# Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test summary
|
||||||
|
|
||||||
|
- Tests added: 2 (`handshake_rejects_v1_protocol_version`, `client_receives_protocol_version_mismatch`)
|
||||||
|
- Tests modified: 0
|
||||||
|
- Workspace test count before: 572 / after: 613 (includes T1.5 android/desktop fixes)
|
||||||
|
- `cargo clippy -p wzp-proto -p wzp-client -p wzp-relay -p wzp-desktop --all-targets -- -D warnings`: pass
|
||||||
|
- `cargo fmt --all -- --check`: pass
|
||||||
|
|
||||||
|
## Risks / follow-ups
|
||||||
|
|
||||||
|
- `wzp-android` requires NDK to link; the Rust source compiles but the crate cannot be fully built on macOS. The T1.5 migration fixes were verified via `cargo check -p wzp-android`.
|
||||||
|
- The `deps/featherchat` submodule has 3 pre-existing clippy errors documented in `PROTOCOL-AUDIT.md`.
|
||||||
|
|
||||||
|
## Reviewer checklist (filled in by reviewer)
|
||||||
|
|
||||||
|
- [ ] Code matches PRD intent
|
||||||
|
- [ ] Verification output is real (re-run if suspicious)
|
||||||
|
- [ ] No backward-incompat surprises
|
||||||
|
- [ ] Tests cover the new behavior
|
||||||
|
- [ ] Approved
|
||||||
Reference in New Issue
Block a user