liminal_protocol/wire/request.rs
1use alloc::vec::Vec;
2
3use super::{
4 AttachAttemptToken, AttachSecret, ClientDiscriminant, ConversationId, DeliverySeq,
5 DetachAttemptToken, EnrollmentToken, Generation, LeaveAttemptToken, ObserverEpoch,
6 ParticipantId, RecordAdmissionAttemptToken,
7};
8
9/// Enrollment request body (`0x0001`).
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct EnrollmentRequest {
12 /// Conversation to enroll in.
13 pub conversation_id: ConversationId,
14 /// Durable single-purpose enrollment token.
15 pub enrollment_token: EnrollmentToken,
16}
17
18/// Credential-bearing attach request body (`0x0002`).
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct CredentialAttachRequest {
21 /// Conversation containing the participant.
22 pub conversation_id: ConversationId,
23 /// Permanent participant identity.
24 pub participant_id: ParticipantId,
25 /// Presented nonzero credential generation.
26 pub capability_generation: Generation,
27 /// Presented attach secret.
28 pub attach_secret: AttachSecret,
29 /// Durable single-purpose attach token.
30 pub attach_attempt_token: AttachAttemptToken,
31 /// Marker accepted atomically by a fenced recovery attach.
32 pub accept_marker_delivery_seq: Option<DeliverySeq>,
33}
34
35/// Explicit detach request body (`0x0003`).
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct DetachRequest {
38 /// Conversation containing the participant.
39 pub conversation_id: ConversationId,
40 /// Permanent participant identity.
41 pub participant_id: ParticipantId,
42 /// Presented nonzero credential generation.
43 pub capability_generation: Generation,
44 /// Durable single-purpose detach token.
45 pub detach_attempt_token: DetachAttemptToken,
46}
47
48/// Continuous cumulative acknowledgement body (`0x0004`).
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct ParticipantAck {
51 /// Conversation containing the participant.
52 pub conversation_id: ConversationId,
53 /// Permanent participant identity.
54 pub participant_id: ParticipantId,
55 /// Presented nonzero credential generation.
56 pub capability_generation: Generation,
57 /// Greatest continuously available sequence being acknowledged.
58 pub through_seq: DeliverySeq,
59}
60
61/// Terminal participant Leave body (`0x0005`).
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct LeaveRequest {
64 /// Conversation containing the participant.
65 pub conversation_id: ConversationId,
66 /// Permanent participant identity.
67 pub participant_id: ParticipantId,
68 /// Presented nonzero credential generation.
69 pub capability_generation: Generation,
70 /// Presented attach secret; it is never echoed in a response envelope.
71 pub attach_secret: AttachSecret,
72 /// Durable single-purpose Leave token.
73 pub leave_attempt_token: LeaveAttemptToken,
74}
75
76/// Explicit marker acknowledgement body (`0x0006`).
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct MarkerAck {
79 /// Conversation containing the participant.
80 pub conversation_id: ConversationId,
81 /// Permanent participant identity.
82 pub participant_id: ParticipantId,
83 /// Presented nonzero credential generation.
84 pub capability_generation: Generation,
85 /// Delivered marker being accepted.
86 pub marker_delivery_seq: DeliverySeq,
87}
88
89/// Ordinary record-admission body (`0x0007`).
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct RecordAdmission {
92 /// Conversation receiving the record.
93 pub conversation_id: ConversationId,
94 /// Verified sender participant.
95 pub participant_id: ParticipantId,
96 /// Presented nonzero credential generation.
97 pub capability_generation: Generation,
98 /// Client-selected identity of this record-admission request attempt.
99 ///
100 /// # It must be minted once per RECORD, not once per presentation
101 ///
102 /// This token is the client's half of ordinary-admission idempotence
103 /// (contract amendment A2, §0.13). The server deduplicates on the identity
104 /// triple (token, payload fingerprint, verified participant), so an
105 /// answer-lost re-present is answered with the original commit only if it
106 /// arrives carrying the SAME token.
107 ///
108 /// Mint it once when the record is staged, persist it beside the staged
109 /// bytes, and re-present that exact token after a lost answer — the
110 /// write-ahead discipline R-C0 already requires for the tokenized families,
111 /// applied one layer up.
112 ///
113 /// Deriving it per presentation from anything that can change between
114 /// attempts silently defeats this. That is not hypothetical: in the field
115 /// (2026-08-08, conversation 6) a client derived the token per presentation
116 /// with the CURRENT capability generation as an input, a recovery attach
117 /// rotated the generation 4 -> 6 between the two presentations, and the
118 /// same bytes arrived under two different tokens — committing a
119 /// byte-identical second copy at a new sequence.
120 ///
121 /// The server cannot close this from its side, and deliberately does not
122 /// try: two intent-distinct sends of the same body also carry distinct
123 /// tokens and MUST remain two commits, so any dedup keyed on payload bytes
124 /// alone would collapse a legitimate pair. Both halves are pinned in
125 /// `tests_record_admission_dedup`.
126 ///
127 /// Re-enrolling changes the verified participant and therefore the triple,
128 /// so it defeats dedup by design.
129 pub record_admission_attempt_token: RecordAdmissionAttemptToken,
130 /// Opaque application payload; it is never echoed in a response envelope.
131 pub payload: Vec<u8>,
132}
133
134/// One observer refusal supplied during reconnect recovery.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub struct ObserverRefusal {
137 /// Refused conversation.
138 pub conversation_id: ConversationId,
139 /// Refusal epoch the SDK needs to arm or classify.
140 pub refused_epoch: ObserverEpoch,
141}
142
143/// One-shot observer-recovery batch body (`0x0008`).
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct ObserverRecoveryHandshake {
146 /// Request-ordered refusal list. Its wire count is the special `u64` count.
147 pub observer_refusals: Vec<ObserverRefusal>,
148}
149
150/// Exhaustive client-to-server participant request.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub enum ClientRequest {
153 /// `0x0001` enrollment request.
154 Enrollment(EnrollmentRequest),
155 /// `0x0002` credential attach request.
156 CredentialAttach(CredentialAttachRequest),
157 /// `0x0003` explicit detach request.
158 Detach(DetachRequest),
159 /// `0x0004` continuous acknowledgement.
160 ParticipantAck(ParticipantAck),
161 /// `0x0005` terminal Leave request.
162 Leave(LeaveRequest),
163 /// `0x0006` marker acknowledgement.
164 MarkerAck(MarkerAck),
165 /// `0x0007` ordinary record admission.
166 RecordAdmission(RecordAdmission),
167 /// `0x0008` reconnect recovery batch.
168 ObserverRecovery(ObserverRecoveryHandshake),
169}
170
171impl ClientRequest {
172 /// Returns the stable request discriminant.
173 #[must_use]
174 pub const fn discriminant(&self) -> ClientDiscriminant {
175 match self {
176 Self::Enrollment(_) => ClientDiscriminant::EnrollmentRequest,
177 Self::CredentialAttach(_) => ClientDiscriminant::CredentialAttachRequest,
178 Self::Detach(_) => ClientDiscriminant::DetachRequest,
179 Self::ParticipantAck(_) => ClientDiscriminant::ParticipantAck,
180 Self::Leave(_) => ClientDiscriminant::LeaveRequest,
181 Self::MarkerAck(_) => ClientDiscriminant::MarkerAck,
182 Self::RecordAdmission(_) => ClientDiscriminant::RecordAdmission,
183 Self::ObserverRecovery(_) => ClientDiscriminant::ObserverRecoveryHandshake,
184 }
185 }
186}