1pub use crate::path::{PathKind, WirePath};
2use base64::{Engine as _, engine::general_purpose::STANDARD};
3use serde::{Deserialize, Serialize};
4#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct Context {
7 pub workspace_id: String,
8 pub client_id: String,
9 pub target_binding: String,
10 pub runtime_generation: String,
11}
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct Header {
15 pub major: u32,
16 pub minor: u32,
17 pub request_id: u64,
18 #[serde(deserialize_with = "required_option")]
19 pub context: Option<Context>,
20 #[serde(deserialize_with = "required_option")]
21 pub operation_id: Option<String>,
22 #[serde(deserialize_with = "required_option")]
23 pub stream_id: Option<String>,
24 #[serde(deserialize_with = "required_option")]
25 pub sequence: Option<u64>,
26 #[serde(flatten)]
27 pub message: Message,
28}
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(tag = "kind", content = "metadata", deny_unknown_fields)]
31pub enum Message {
32 #[serde(rename = "proof.lookup.begin")]
33 ProofLookupBegin {
34 transfer_id: String,
35 total_length: u64,
36 action: ProofAction,
37 lease_id: String,
38 },
39 #[serde(rename = "proof.lookup.ready")]
40 ProofLookupReady { credit: u32 },
41 #[serde(rename = "proof.ready")]
42 ProofReady {
43 original_transfer_id: String,
44 approvals_transfer_id: String,
45 },
46 #[serde(rename = "proof.observe.begin")]
47 ProofObserveBegin {
48 transfer_id: String,
49 total_length: u64,
50 lease_id: String,
51 worktree: crate::services::Worktree,
52 },
53 #[serde(rename = "proof.approvals.begin")]
54 ProofApprovalsBegin {
55 transfer_id: String,
56 total_length: u64,
57 proof_id: String,
58 alias_digest: String,
59 },
60 #[serde(rename = "proof.output.begin")]
61 ProofOutputBegin {
62 transfer_id: String,
63 total_length: u64,
64 },
65 #[serde(rename = "proof.prepared")]
66 ProofPrepared { plan_id: String },
67 #[serde(rename = "proof.existing")]
68 ProofExisting { receipt: crate::services::Receipt },
69 #[serde(rename = "proof.missing")]
70 ProofMissing {},
71 #[serde(rename = "proof.cancel")]
72 ProofCancel { lease_id: String, plan_id: String },
73 #[serde(rename = "proof.release")]
74 ProofRelease {
75 lease_id: String,
76 observation_id: String,
77 },
78 #[serde(rename = "proof.lookup.chunk")]
79 ProofLookupChunk { transfer_id: String, offset: u64 },
80 #[serde(rename = "proof.lookup.end")]
81 ProofLookupEnd { transfer_id: String, sha256: String },
82 #[serde(rename = "proof.original.chunk")]
83 ProofOriginalChunk { transfer_id: String, offset: u64 },
84 #[serde(rename = "proof.original.end")]
85 ProofOriginalEnd { transfer_id: String, sha256: String },
86 #[serde(rename = "proof.approvals.chunk")]
87 ProofApprovalsChunk { transfer_id: String, offset: u64 },
88 #[serde(rename = "proof.approvals.end")]
89 ProofApprovalsEnd { transfer_id: String, sha256: String },
90 #[serde(rename = "proof.aliases.chunk")]
91 ProofAliasesChunk { transfer_id: String, offset: u64 },
92 #[serde(rename = "proof.aliases.end")]
93 ProofAliasesEnd { transfer_id: String, sha256: String },
94 #[serde(rename = "proof.output.chunk")]
95 ProofOutputChunk { transfer_id: String, offset: u64 },
96 #[serde(rename = "proof.output.end")]
97 ProofOutputEnd { transfer_id: String, sha256: String },
98 #[serde(rename = "hello")]
99 Hello {
100 capabilities: Vec<String>,
101 max_header: u32,
102 max_payload: u32,
103 },
104 #[serde(rename = "welcome")]
105 Welcome {
106 capabilities: Vec<String>,
107 max_header: u32,
108 max_payload: u32,
109 initial_credit: u32,
110 target_binding: String,
111 runtime_generation: String,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 container_start_base64: Option<String>,
114 },
115 #[serde(rename = "lease.register")]
116 Register {
117 lease_id: String,
118 client_id: String,
119 workspace_id: String,
120 target_binding: String,
121 runtime_generation: String,
122 },
123 #[serde(rename = "lease.release")]
124 Release {
125 lease_id: String,
126 intent: ReleaseIntent,
127 },
128 #[serde(rename = "connection.authentication")]
129 Authentication {
130 #[serde(deserialize_with = "required_option")]
131 ssh_auth_sock: Option<String>,
132 },
133 #[serde(rename = "ack")]
134 Ack {},
135 #[serde(rename = "stream.credit")]
136 Credit { bytes: u32 },
137 #[serde(rename = "file.read")]
138 Read { path_base64: String },
139 #[serde(rename = "operation.receipt")]
140 Receipt {
141 state: ReceiptState,
142 #[serde(deserialize_with = "required_option")]
143 saved_version: Option<WireVersion>,
144 #[serde(deserialize_with = "required_option")]
145 saved_revision: Option<u64>,
146 safe_message: String,
147 #[serde(deserialize_with = "required_option")]
148 failure: Option<WireFailure>,
149 observed: WireObservation,
150 },
151 #[serde(rename = "cancel.result")]
152 Cancellation { outcome: CancelOutcome },
153 #[serde(rename = "service.call")]
154 ServiceCall { call: crate::services::Call },
155 #[serde(rename = "service.event")]
156 ServiceEvent {
157 offset: u64,
158 total_length: u64,
159 sha256: String,
160 terminal: bool,
161 },
162 #[serde(rename = "file.list")]
163 FileList { path_base64: String },
164 #[serde(rename = "file.read.begin")]
165 ReadBegin {
166 transfer_id: String,
167 path_base64: String,
168 version: String,
169 total_length: u64,
170 },
171 #[serde(rename = "file.read.chunk")]
172 ReadChunk { transfer_id: String, offset: u64 },
173 #[serde(rename = "file.read.end")]
174 ReadEnd { transfer_id: String, sha256: String },
175 #[serde(rename = "file.upload.begin")]
176 UploadBegin {
177 transfer_id: String,
178 path_base64: String,
179 total_length: u64,
180 },
181 #[serde(rename = "file.upload.chunk")]
182 UploadChunk { transfer_id: String, offset: u64 },
183 #[serde(rename = "file.upload.end")]
184 UploadEnd { transfer_id: String, sha256: String },
185 #[serde(rename = "file.create.commit")]
186 CreateCommit { transfer_id: String },
187 #[serde(rename = "file.save.commit")]
188 SaveCommit {
189 transfer_id: String,
190 submitted_revision: u64,
191 #[serde(deserialize_with = "required_option")]
192 expected_version: Option<String>,
193 },
194 #[serde(rename = "file.save.commit_owned")]
195 SaveCommitOwned {
196 transfer_id: String,
197 lease_id: String,
198 submitted_revision: u64,
199 #[serde(deserialize_with = "required_option")]
200 expected_version: Option<String>,
201 },
202 #[serde(rename = "operation.inspect")]
203 Inspect { operation_id: String },
204 #[serde(rename = "terminal.output")]
205 TerminalOutput { session_id: String },
206 #[serde(rename = "cancel")]
207 Cancel { request_id: u64 },
208 #[serde(rename = "error")]
209 Error { code: String, safe_message: String },
210}
211#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum ProofAction {
214 Lookup,
215 Prepare,
216 Execute,
217}
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum ReleaseIntent {
221 ViewDetachment,
222 ResolvedWorkspaceClosure,
223}
224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub enum ReceiptState {
227 Accepted,
228 Running,
229 Confirmed,
230 Failed,
231 Unknown,
232}
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct WireVersion {
236 pub opaque_token: String,
237 pub content_digest: String,
238 pub file_identity: String,
239}
240#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242pub enum CancelOutcome {
243 ConfirmedCancel,
244 Unknown,
245}
246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
247#[serde(rename_all = "snake_case")]
248pub enum WireEffect {
249 ProvenNoEffect,
250 PossiblePartialEffects,
251}
252#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
253#[serde(deny_unknown_fields)]
254pub struct WireFailure {
255 pub code: String,
256 pub effect: WireEffect,
257}
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
260pub enum WireObservation {
261 Unobserved,
262 File {
263 path_base64: String,
264 #[serde(deserialize_with = "required_option")]
265 version: Option<WireVersion>,
266 #[serde(deserialize_with = "required_option")]
267 digest: Option<String>,
268 },
269}
270pub fn decode_path(value: &str) -> Result<Vec<u8>, &'static str> {
271 let bytes = STANDARD.decode(value).map_err(|_| "invalid base64 path")?;
272 if bytes.first() != Some(&b'/') || bytes.contains(&0) {
273 return Err("invalid absolute POSIX path");
274 }
275 Ok(bytes)
276}
277pub fn uuid(value: &str) -> bool {
278 value.len() == 36
279 && value.bytes().enumerate().all(|(i, c)| {
280 if [8, 13, 18, 23].contains(&i) {
281 c == b'-'
282 } else {
283 c.is_ascii_hexdigit()
284 }
285 })
286}
287impl Header {
288 pub fn validate(&self, payload: usize, expected: Option<&Context>) -> Result<(), &'static str> {
289 if self.major != 1 {
290 return Err("unsupported major");
291 }
292 if self.request_id == 0 {
293 return Err("missing request ID");
294 }
295 if let Message::Hello {
296 max_header,
297 max_payload,
298 ..
299 } = &self.message
300 {
301 if self.context.is_some()
302 || self.operation_id.is_some()
303 || self.stream_id.is_some()
304 || self.sequence.is_some()
305 || payload != 0
306 || *max_header == 0
307 || *max_header > 65536
308 || *max_payload == 0
309 || *max_payload > 1048576
310 {
311 return Err("invalid negotiation limits");
312 }
313 return Ok(());
314 }
315 if let Message::Welcome {
316 max_header,
317 max_payload,
318 initial_credit,
319 target_binding,
320 runtime_generation,
321 container_start_base64,
322 capabilities,
323 ..
324 } = &self.message
325 {
326 if self.context.is_some()
327 || self.operation_id.is_some()
328 || self.stream_id.is_some()
329 || self.sequence.is_some()
330 || payload != 0
331 || *max_header == 0
332 || *max_header > 65536
333 || *max_payload == 0
334 || *max_payload > 1048576
335 || *initial_credit != 524288
336 || target_binding.is_empty()
337 || !uuid(runtime_generation)
338 || decode_container_start(container_start_base64.as_deref()).is_err()
339 || container_start_base64.is_some()
340 && !capabilities.iter().any(|c| c == "container.binding.v1")
341 {
342 return Err("invalid welcome");
343 }
344 return Ok(());
345 }
346 if let Message::Register {
347 lease_id,
348 client_id,
349 workspace_id,
350 target_binding,
351 runtime_generation,
352 } = &self.message
353 {
354 if self.context.is_some()
355 || self.operation_id.is_some()
356 || self.stream_id.is_some()
357 || self.sequence.is_some()
358 || payload != 0
359 || !uuid(lease_id)
360 || !uuid(client_id)
361 || !uuid(workspace_id)
362 || target_binding.is_empty()
363 || !uuid(runtime_generation)
364 {
365 return Err("invalid registration");
366 }
367 return Ok(());
369 }
370 let context = self.context.as_ref().ok_or("missing scoped context")?;
371 if Some(context) != expected {
372 return Err("mismatched authenticated context");
373 }
374 if !uuid(&context.workspace_id)
375 || !uuid(&context.client_id)
376 || context.target_binding.is_empty()
377 || context.runtime_generation.is_empty()
378 {
379 return Err("invalid scoped IDs");
380 }
381 for id in [&self.operation_id, &self.stream_id].into_iter().flatten() {
382 if !uuid(id) {
383 return Err("invalid ID");
384 }
385 }
386 let raw = matches!(
387 self.message,
388 Message::ServiceEvent { .. }
389 | Message::ProofLookupChunk { .. }
390 | Message::ProofOriginalChunk { .. }
391 | Message::ProofApprovalsChunk { .. }
392 | Message::ProofAliasesChunk { .. }
393 | Message::ProofOutputChunk { .. }
394 | Message::ReadChunk { .. }
395 | Message::UploadChunk { .. }
396 | Message::TerminalOutput { .. }
397 | Message::ServiceCall {
398 call: crate::services::Call::TerminalInput { .. }
399 }
400 );
401 if payload != 0 && !raw {
402 return Err("unexpected payload");
403 }
404 if raw && payload > 65536 {
405 return Err("chunk too large");
406 }
407 match &self.message {
408 Message::ProofLookupBegin {
409 transfer_id,
410 lease_id,
411 ..
412 } => {
413 if !uuid(lease_id)
414 || self.operation_id.is_none()
415 || self.sequence.is_some()
416 || self.stream_id.as_ref() != Some(transfer_id)
417 {
418 return Err("invalid proof lookup identity");
419 }
420 }
421 Message::ProofObserveBegin {
422 transfer_id,
423 lease_id,
424 ..
425 } => {
426 if !uuid(lease_id)
427 || self.operation_id.is_some()
428 || self.sequence.is_some()
429 || self.stream_id.as_ref() != Some(transfer_id)
430 {
431 return Err("invalid observation identity");
432 }
433 }
434 Message::ProofApprovalsBegin {
435 transfer_id,
436 proof_id,
437 alias_digest,
438 ..
439 } => {
440 if !uuid(proof_id)
441 || !hex_digest(alias_digest)
442 || self.operation_id.is_none()
443 || self.sequence.is_some()
444 || self.stream_id.as_ref() != Some(transfer_id)
445 {
446 return Err("invalid approval identity");
447 }
448 }
449 Message::ProofLookupReady { credit }
450 if *credit != 65536 || self.stream_id.is_none() || self.operation_id.is_none() =>
451 {
452 return Err("invalid lookup credit");
453 }
454 Message::ProofReady {
455 original_transfer_id,
456 approvals_transfer_id,
457 } => {
458 if !uuid(original_transfer_id)
459 || !uuid(approvals_transfer_id)
460 || original_transfer_id == approvals_transfer_id
461 || self.operation_id.is_none()
462 {
463 return Err("invalid proof ready identity");
464 }
465 }
466 Message::ProofOutputBegin { transfer_id, .. }
467 if self.stream_id.as_ref() != Some(transfer_id) || self.operation_id.is_some() =>
468 {
469 return Err("invalid proof output identity");
470 }
471 Message::ProofPrepared { plan_id } if !uuid(plan_id) || self.operation_id.is_none() => {
472 return Err("invalid plan identity");
473 }
474 Message::ProofExisting { .. } if self.operation_id.is_none() => {
475 return Err("missing proof receipt operation");
476 }
477 Message::ProofMissing {}
478 if self.operation_id.is_none()
479 || self.stream_id.is_none()
480 || self.sequence.is_some() =>
481 {
482 return Err("invalid missing-original reply identity");
483 }
484 Message::ProofCancel { lease_id, plan_id } => {
485 if !uuid(lease_id)
486 || !uuid(plan_id)
487 || self.operation_id.is_some()
488 || self.stream_id.is_some()
489 || self.sequence.is_some()
490 {
491 return Err("invalid plan cancellation");
492 }
493 }
494 Message::ProofRelease {
495 lease_id,
496 observation_id,
497 } => {
498 if !uuid(lease_id)
499 || !uuid(observation_id)
500 || self.operation_id.is_some()
501 || self.stream_id.is_some()
502 || self.sequence.is_some()
503 {
504 return Err("invalid observation release");
505 }
506 }
507 Message::ProofLookupChunk { transfer_id, .. }
508 | Message::ProofOriginalChunk { transfer_id, .. }
509 | Message::ProofApprovalsChunk { transfer_id, .. }
510 | Message::ProofAliasesChunk { transfer_id, .. }
511 | Message::ProofOutputChunk { transfer_id, .. } => {
512 if self.stream_id.as_ref() != Some(transfer_id)
513 || self.sequence.is_none()
514 || payload == 0
515 || payload > 65536
516 {
517 return Err("invalid proof frame");
518 }
519 if matches!(
520 self.message,
521 Message::ProofLookupChunk { .. }
522 | Message::ProofOriginalChunk { .. }
523 | Message::ProofApprovalsChunk { .. }
524 ) && self.operation_id.is_none()
525 {
526 return Err("missing original operation");
527 }
528 }
529 Message::ProofLookupEnd {
530 transfer_id,
531 sha256,
532 }
533 | Message::ProofOriginalEnd {
534 transfer_id,
535 sha256,
536 }
537 | Message::ProofApprovalsEnd {
538 transfer_id,
539 sha256,
540 }
541 | Message::ProofAliasesEnd {
542 transfer_id,
543 sha256,
544 }
545 | Message::ProofOutputEnd {
546 transfer_id,
547 sha256,
548 } => {
549 if self.stream_id.as_ref() != Some(transfer_id)
550 || self.sequence.is_none()
551 || !hex_digest(sha256)
552 {
553 return Err("invalid proof frame");
554 }
555 if matches!(
556 self.message,
557 Message::ProofLookupEnd { .. }
558 | Message::ProofOriginalEnd { .. }
559 | Message::ProofApprovalsEnd { .. }
560 ) && self.operation_id.is_none()
561 {
562 return Err("missing original operation");
563 }
564 }
565 Message::Authentication {
566 ssh_auth_sock: Some(path),
567 } => {
568 decode_path(path)?;
569 }
570 Message::ServiceCall {
571 call: crate::services::Call::LeaseObserve { owner },
572 } => {
573 owner.validate().map_err(|_| "invalid observed owner")?;
574 if self.operation_id.is_some()
575 || self.stream_id.is_some()
576 || self.sequence.is_some()
577 {
578 return Err("unexpected observation metadata");
579 }
580 }
581 Message::ServiceCall { call }
582 if (call.mutating()
583 || matches!(
584 call,
585 crate::services::Call::Inspect | crate::services::Call::GitExisting { .. }
586 ))
587 && self.operation_id.is_none() =>
588 {
589 return Err("missing service operation ID");
590 }
591 Message::ServiceEvent {
592 offset,
593 total_length,
594 sha256,
595 ..
596 } if self.stream_id.is_none()
597 || self.sequence.is_none()
598 || *total_length > 32 * 1024 * 1024
599 || offset.saturating_add(payload as u64) > *total_length
600 || sha256.len() != 64
601 || !sha256.bytes().all(|b| b.is_ascii_hexdigit()) =>
602 {
603 return Err("invalid service event");
604 }
605 Message::Release { lease_id, .. } if !uuid(lease_id) => return Err("invalid lease ID"),
606 Message::Credit { bytes }
607 if *bytes == 0 || *bytes > 524288 || self.stream_id.is_none() =>
608 {
609 return Err("invalid stream credit");
610 }
611 Message::Receipt { .. } if self.operation_id.is_none() => {
612 return Err("missing receipt operation");
613 }
614 Message::Cancel { request_id: 0 } => return Err("invalid cancellation target ID"),
615 Message::ReadBegin {
616 transfer_id,
617 path_base64,
618 ..
619 }
620 | Message::UploadBegin {
621 transfer_id,
622 path_base64,
623 ..
624 } => {
625 if !uuid(transfer_id) {
626 return Err("invalid transfer ID");
627 }
628 decode_path(path_base64)?;
629 }
630 Message::FileList { path_base64 } | Message::Read { path_base64 } => {
631 decode_path(path_base64)?;
632 }
633 Message::ReadChunk { transfer_id, .. }
634 | Message::UploadChunk { transfer_id, .. }
635 | Message::CreateCommit { transfer_id }
636 | Message::SaveCommit { transfer_id, .. } => {
637 if !uuid(transfer_id) {
638 return Err("invalid transfer ID");
639 }
640 }
641 Message::SaveCommitOwned {
642 transfer_id,
643 lease_id,
644 ..
645 } => {
646 if !uuid(transfer_id) || !uuid(lease_id) {
647 return Err("invalid owned save transfer or lease ID");
648 }
649 }
650 Message::ReadEnd {
651 transfer_id,
652 sha256,
653 }
654 | Message::UploadEnd {
655 transfer_id,
656 sha256,
657 } => {
658 if !uuid(transfer_id)
659 || sha256.len() != 64
660 || !sha256.bytes().all(|b| b.is_ascii_hexdigit())
661 {
662 return Err("invalid transfer digest");
663 }
664 }
665 Message::Inspect { operation_id } => {
666 if !uuid(operation_id) || self.operation_id.as_ref() != Some(operation_id) {
667 return Err("mismatched operation ID");
668 }
669 }
670 Message::TerminalOutput { session_id }
671 if (!uuid(session_id) || self.sequence.is_none() || self.stream_id.is_none()) =>
672 {
673 return Err("invalid terminal stream");
674 }
675 _ => {}
676 }
677 if matches!(
678 self.message,
679 Message::UploadBegin { .. }
680 | Message::UploadChunk { .. }
681 | Message::UploadEnd { .. }
682 | Message::CreateCommit { .. }
683 | Message::SaveCommit { .. }
684 | Message::SaveCommitOwned { .. }
685 ) && self.operation_id.is_none()
686 {
687 return Err("missing operation ID");
688 }
689 Ok(())
690 }
691}
692pub fn decode_container_start(value: Option<&str>) -> Result<Option<Vec<u8>>, &'static str> {
693 value
694 .map(|s| {
695 if s.len() > 344 {
696 return Err("container start generation exceeded its bound");
697 }
698 let bytes = STANDARD
699 .decode(s)
700 .map_err(|_| "invalid container start generation")?;
701 if bytes.is_empty() || bytes.len() > 256 {
702 return Err("invalid container start generation length");
703 }
704 Ok(bytes)
705 })
706 .transpose()
707}
708
709pub(crate) fn required_option<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
710where
711 D: serde::Deserializer<'de>,
712 T: Deserialize<'de>,
713{
714 Option::<T>::deserialize(deserializer)
715}
716
717fn hex_digest(s: &str) -> bool {
718 s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
719}