1use std::fmt;
2use std::ops::Range;
3
4use gaze::{
5 CommittedSessionSnapshot, PrefixCacheWriteMode, RestoreError, RestoredTextWithProvenance,
6};
7use gaze_types::inspection::{
8 JsonShapeSummaryV1, ProjectionAvailabilityV1, ProjectionOmissionReasonV1, SseTimelineMetaV1,
9};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum WireFormat {
14 Empty,
15 Json,
16 Ndjson,
17 Utf8Text,
18 Sse,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum CodecPhase {
24 RequestParse,
25 RequestProtection,
26 RequestProof,
27 ResponseParse,
28 ResponseRestore,
29 ResponseProof,
30 SseDecode,
31 SseLifecycle,
32 SseReplay,
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum CodecErrorCode {
38 InvalidUtf8,
39 InvalidJson,
40 InvalidWireFormat,
41 DuplicateObjectKey,
42 DepthLimitExceeded,
43 NodeLimitExceeded,
44 StringLimitExceeded,
45 NumberLimitExceeded,
46 BodyLimitExceeded,
47 GrowthLimitExceeded,
48 InternalCoverageFailure,
49 UnsupportedRequestSurface,
50 UnsupportedResponseSurface,
51 ControlWouldMutate,
52 OpaqueMediaUninspected,
53 SignedMutationRequired,
54 SignedSurfaceMalformed,
55 ProtectionFailedClosed,
56 RestoreFailedClosed,
57 ProviderOriginNumeric,
58 ProviderOriginPii,
59 TransformedKeyCollision,
60 InvalidSseField,
61 InvalidSseFrame,
62 InvalidSseEventPair,
63 SseLineLimitExceeded,
64 SseFrameLimitExceeded,
65 SseEventLimitExceeded,
66 SseIndexLimitExceeded,
67 SseAccumulatorLimitExceeded,
68 InvalidSseLifecycle,
69 InvalidContentBlockIndex,
70 InvalidContentBlockDelta,
71 IncompleteSseStream,
72 UnsafeFutureEvent,
73 ProviderErrorEvent,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78#[non_exhaustive]
79pub enum OpaqueCarrierScheme {
80 Data,
81 File,
82 Http,
83 Https,
84}
85
86impl OpaqueCarrierScheme {
87 #[must_use]
89 pub const fn as_str(self) -> &'static str {
90 match self {
91 Self::Data => "data:",
92 Self::File => "file:",
93 Self::Http => "http:",
94 Self::Https => "https:",
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101#[non_exhaustive]
102pub struct OpaqueCarrierLocation {
103 scheme: OpaqueCarrierScheme,
104 byte_offset: usize,
105 byte_length: usize,
106 content_block_index: Option<usize>,
107}
108
109impl OpaqueCarrierLocation {
110 pub(crate) const fn new(
111 scheme: OpaqueCarrierScheme,
112 byte_offset: usize,
113 byte_length: usize,
114 content_block_index: Option<usize>,
115 ) -> Self {
116 Self {
117 scheme,
118 byte_offset,
119 byte_length,
120 content_block_index,
121 }
122 }
123
124 #[must_use]
125 pub const fn scheme(self) -> OpaqueCarrierScheme {
126 self.scheme
127 }
128
129 #[must_use]
130 pub const fn byte_offset(self) -> usize {
131 self.byte_offset
132 }
133
134 #[must_use]
135 pub const fn byte_length(self) -> usize {
136 self.byte_length
137 }
138
139 #[must_use]
140 pub const fn content_block_index(self) -> Option<usize> {
141 self.content_block_index
142 }
143}
144
145#[derive(Clone, Copy, Eq, PartialEq)]
147pub struct CodecError {
148 code: CodecErrorCode,
149 phase: CodecPhase,
150 opaque_carrier: Option<OpaqueCarrierLocation>,
151}
152
153impl CodecError {
154 pub(crate) const fn new(code: CodecErrorCode, phase: CodecPhase) -> Self {
155 Self {
156 code,
157 phase,
158 opaque_carrier: None,
159 }
160 }
161
162 pub(crate) const fn with_opaque_carrier(
163 mut self,
164 opaque_carrier: Option<OpaqueCarrierLocation>,
165 ) -> Self {
166 self.opaque_carrier = opaque_carrier;
167 self
168 }
169
170 #[must_use]
171 pub const fn code(self) -> CodecErrorCode {
172 self.code
173 }
174
175 #[must_use]
176 pub const fn phase(self) -> CodecPhase {
177 self.phase
178 }
179
180 #[must_use]
182 pub const fn opaque_carrier(self) -> Option<OpaqueCarrierLocation> {
183 self.opaque_carrier
184 }
185}
186
187impl fmt::Debug for CodecError {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 let mut debug = formatter.debug_struct("CodecError");
190 debug.field("code", &self.code).field("phase", &self.phase);
191 if let Some(opaque_carrier) = self.opaque_carrier {
192 debug.field("opaque_carrier", &opaque_carrier);
193 }
194 debug.finish()
195 }
196}
197
198impl fmt::Display for CodecError {
199 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(formatter, "codec_{:?}_{:?}", self.phase, self.code)
201 }
202}
203
204impl std::error::Error for CodecError {}
205
206#[derive(Clone, Copy, Debug, Eq, PartialEq)]
208pub struct CodecLimits {
209 max_body_bytes: usize,
210 max_json_depth: usize,
211 max_json_nodes: usize,
212 max_string_bytes: usize,
213 max_number_bytes: usize,
214 max_growth_numerator: usize,
215 max_growth_denominator: usize,
216 max_sse_line_bytes: usize,
217 max_sse_frame_bytes: usize,
218 max_sse_events: usize,
219 max_sse_indices: usize,
220 max_sse_accumulator_bytes: usize,
221}
222
223impl Default for CodecLimits {
224 fn default() -> Self {
225 Self {
226 max_body_bytes: 32 * 1024 * 1024,
227 max_json_depth: 128,
228 max_json_nodes: 100_000,
229 max_string_bytes: 4 * 1024 * 1024,
230 max_number_bytes: 128,
231 max_growth_numerator: 2,
232 max_growth_denominator: 1,
233 max_sse_line_bytes: 256 * 1024,
234 max_sse_frame_bytes: 1024 * 1024,
235 max_sse_events: 10_000,
236 max_sse_indices: 256,
237 max_sse_accumulator_bytes: 8 * 1024 * 1024,
238 }
239 }
240}
241
242impl CodecLimits {
243 #[must_use]
244 pub const fn with_max_body_bytes(mut self, value: usize) -> Self {
245 self.max_body_bytes = value;
246 self
247 }
248
249 #[must_use]
250 pub const fn with_max_json_depth(mut self, value: usize) -> Self {
251 self.max_json_depth = value;
252 self
253 }
254
255 #[must_use]
256 pub const fn with_max_json_nodes(mut self, value: usize) -> Self {
257 self.max_json_nodes = value;
258 self
259 }
260
261 #[must_use]
262 pub const fn with_max_string_bytes(mut self, value: usize) -> Self {
263 self.max_string_bytes = value;
264 self
265 }
266
267 #[must_use]
268 pub const fn with_max_number_bytes(mut self, value: usize) -> Self {
269 self.max_number_bytes = value;
270 self
271 }
272
273 #[must_use]
274 pub const fn with_max_sse_line_bytes(mut self, value: usize) -> Self {
275 self.max_sse_line_bytes = value;
276 self
277 }
278
279 #[must_use]
280 pub const fn with_max_sse_frame_bytes(mut self, value: usize) -> Self {
281 self.max_sse_frame_bytes = value;
282 self
283 }
284
285 #[must_use]
286 pub const fn with_max_sse_events(mut self, value: usize) -> Self {
287 self.max_sse_events = value;
288 self
289 }
290
291 #[must_use]
292 pub const fn with_max_sse_indices(mut self, value: usize) -> Self {
293 self.max_sse_indices = value;
294 self
295 }
296
297 #[must_use]
298 pub const fn with_max_sse_accumulator_bytes(mut self, value: usize) -> Self {
299 self.max_sse_accumulator_bytes = value;
300 self
301 }
302
303 #[must_use]
304 pub const fn max_body_bytes(self) -> usize {
305 self.max_body_bytes
306 }
307
308 #[must_use]
309 pub const fn max_json_depth(self) -> usize {
310 self.max_json_depth
311 }
312
313 #[must_use]
314 pub const fn max_json_nodes(self) -> usize {
315 self.max_json_nodes
316 }
317
318 #[must_use]
319 pub const fn max_string_bytes(self) -> usize {
320 self.max_string_bytes
321 }
322
323 #[must_use]
324 pub const fn max_number_bytes(self) -> usize {
325 self.max_number_bytes
326 }
327
328 #[must_use]
329 pub const fn max_sse_line_bytes(self) -> usize {
330 self.max_sse_line_bytes
331 }
332
333 #[must_use]
334 pub const fn max_sse_frame_bytes(self) -> usize {
335 self.max_sse_frame_bytes
336 }
337
338 #[must_use]
339 pub const fn max_sse_events(self) -> usize {
340 self.max_sse_events
341 }
342
343 #[must_use]
344 pub const fn max_sse_indices(self) -> usize {
345 self.max_sse_indices
346 }
347
348 #[must_use]
349 pub const fn max_sse_accumulator_bytes(self) -> usize {
350 self.max_sse_accumulator_bytes
351 }
352
353 pub(crate) fn output_within_growth_limit(self, input: usize, output: usize) -> bool {
354 let proportional = input
355 .checked_mul(self.max_growth_numerator)
356 .map(|value| value / self.max_growth_denominator)
357 .unwrap_or(usize::MAX);
358 output <= self.max_body_bytes && output <= proportional.max(input)
359 }
360}
361
362pub trait RequestPseudonymizer {
364 fn protect(&mut self, input: &str) -> Result<String, CodecErrorCode>;
366
367 fn protect_with_prefix_cache_write_mode(
374 &mut self,
375 input: &str,
376 mode: PrefixCacheWriteMode,
377 ) -> Result<String, CodecErrorCode> {
378 match mode {
379 PrefixCacheWriteMode::Allow => self.protect(input),
380 PrefixCacheWriteMode::Suppress => Err(CodecErrorCode::ProtectionFailedClosed),
381 }
382 }
383
384 fn validate_token_shapes(&mut self, input: &str) -> Result<(), CodecErrorCode>;
386}
387
388pub trait ResponseResidualValidator {
390 fn validate(
393 &mut self,
394 value: &str,
395 authorized_output_ranges: &[Range<usize>],
396 ) -> Result<(), CodecErrorCode>;
397}
398
399pub struct RequestTransformContext<'a> {
401 pseudonymizer: &'a mut dyn RequestPseudonymizer,
402 format: WireFormat,
403 limits: CodecLimits,
404 inspection_projection_requested: bool,
405 content_block_index: Option<usize>,
406 opaque_carrier: Option<OpaqueCarrierLocation>,
407}
408
409impl<'a> RequestTransformContext<'a> {
410 #[must_use]
411 pub(crate) fn new(
412 pseudonymizer: &'a mut dyn RequestPseudonymizer,
413 format: WireFormat,
414 limits: CodecLimits,
415 ) -> Self {
416 Self {
417 pseudonymizer,
418 format,
419 limits,
420 inspection_projection_requested: false,
421 content_block_index: None,
422 opaque_carrier: None,
423 }
424 }
425
426 #[must_use]
427 pub const fn format(&self) -> WireFormat {
428 self.format
429 }
430
431 #[must_use]
432 pub const fn limits(&self) -> CodecLimits {
433 self.limits
434 }
435
436 pub(crate) fn request_inspection_projection(&mut self) {
437 self.inspection_projection_requested = true;
438 }
439
440 pub(crate) const fn inspection_projection_requested(&self) -> bool {
441 self.inspection_projection_requested
442 }
443
444 pub(crate) fn replace_content_block_index(
445 &mut self,
446 content_block_index: Option<usize>,
447 ) -> Option<usize> {
448 std::mem::replace(&mut self.content_block_index, content_block_index)
449 }
450
451 pub(crate) fn record_opaque_carrier(
452 &mut self,
453 scheme: OpaqueCarrierScheme,
454 byte_offset: usize,
455 byte_length: usize,
456 ) {
457 self.opaque_carrier.get_or_insert_with(|| {
458 OpaqueCarrierLocation::new(scheme, byte_offset, byte_length, self.content_block_index)
459 });
460 }
461
462 pub(crate) fn clear_opaque_carrier(&mut self) {
463 self.opaque_carrier = None;
464 }
465
466 pub(crate) const fn opaque_carrier(&self) -> Option<OpaqueCarrierLocation> {
467 self.opaque_carrier
468 }
469
470 pub(crate) fn protect(&mut self, input: &str) -> Result<String, CodecErrorCode> {
471 self.pseudonymizer
472 .protect_with_prefix_cache_write_mode(input, PrefixCacheWriteMode::Allow)
473 }
474
475 pub(crate) fn probe_without_prefix_cache_write(
477 &mut self,
478 input: &str,
479 ) -> Result<String, CodecErrorCode> {
480 self.pseudonymizer
481 .protect_with_prefix_cache_write_mode(input, PrefixCacheWriteMode::Suppress)
482 }
483
484 pub(crate) fn validate_token_shapes(&mut self, input: &str) -> Result<(), CodecErrorCode> {
485 self.pseudonymizer.validate_token_shapes(input)
486 }
487}
488
489pub struct ResponseTransformContext<'a> {
491 snapshot: &'a CommittedSessionSnapshot,
492 residual: &'a mut dyn ResponseResidualValidator,
493 format: WireFormat,
494 limits: CodecLimits,
495 inspection_projection_requested: bool,
496}
497
498impl<'a> ResponseTransformContext<'a> {
499 #[must_use]
500 pub(crate) fn new(
501 snapshot: &'a CommittedSessionSnapshot,
502 residual: &'a mut dyn ResponseResidualValidator,
503 format: WireFormat,
504 limits: CodecLimits,
505 ) -> Self {
506 Self {
507 snapshot,
508 residual,
509 format,
510 limits,
511 inspection_projection_requested: false,
512 }
513 }
514
515 #[must_use]
516 pub const fn format(&self) -> WireFormat {
517 self.format
518 }
519
520 #[must_use]
521 pub const fn limits(&self) -> CodecLimits {
522 self.limits
523 }
524
525 pub(crate) fn request_inspection_projection(&mut self) {
526 self.inspection_projection_requested = true;
527 }
528
529 pub(crate) const fn inspection_projection_requested(&self) -> bool {
530 self.inspection_projection_requested
531 }
532
533 pub(crate) fn restore(
534 &mut self,
535 input: &str,
536 ) -> Result<RestoredTextWithProvenance, CodecErrorCode> {
537 self.snapshot
538 .restore_strict_text_with_provenance(input)
539 .map_err(map_restore_error)
540 }
541
542 pub(crate) fn validate(
543 &mut self,
544 value: &str,
545 authorized_output_ranges: &[Range<usize>],
546 ) -> Result<(), CodecErrorCode> {
547 self.residual.validate(value, authorized_output_ranges)
548 }
549}
550
551fn map_restore_error(_error: RestoreError) -> CodecErrorCode {
552 CodecErrorCode::RestoreFailedClosed
553}
554
555#[derive(Clone, Default, Eq, PartialEq)]
557pub struct OutputProvenance {
558 authorized_output_ranges: Vec<Range<usize>>,
559 signed_opaque_ranges: Vec<Range<usize>>,
560 occurrence_count: usize,
561 final_buffer_verified: bool,
562}
563
564impl OutputProvenance {
565 #[must_use]
566 pub fn authorized_output_ranges(&self) -> &[Range<usize>] {
567 &self.authorized_output_ranges
568 }
569
570 #[must_use]
571 pub fn signed_opaque_ranges(&self) -> &[Range<usize>] {
572 &self.signed_opaque_ranges
573 }
574
575 #[must_use]
576 pub const fn occurrence_count(&self) -> usize {
577 self.occurrence_count
578 }
579
580 #[must_use]
581 pub const fn final_buffer_verified(&self) -> bool {
582 self.final_buffer_verified
583 }
584
585 pub(crate) fn record_authorized(&mut self, range: Range<usize>) {
586 push_coalesced(&mut self.authorized_output_ranges, range);
587 }
588
589 pub(crate) fn record_signed(&mut self, range: Range<usize>) {
590 push_coalesced(&mut self.signed_opaque_ranges, range);
591 }
592
593 pub(crate) fn set_occurrence_count(&mut self, count: usize) {
594 self.occurrence_count = count;
595 }
596
597 pub(crate) fn mark_final_buffer_verified(&mut self) {
598 self.final_buffer_verified = true;
599 }
600}
601
602fn push_coalesced(ranges: &mut Vec<Range<usize>>, range: Range<usize>) {
603 if range.start >= range.end {
604 return;
605 }
606 if let Some(previous) = ranges.last_mut() {
607 if range.start <= previous.end {
608 previous.end = previous.end.max(range.end);
609 return;
610 }
611 }
612 ranges.push(range);
613}
614
615pub(crate) struct InspectionStructuralProjectionV1 {
617 pub(crate) json_shape: ProjectionAvailabilityV1<JsonShapeSummaryV1>,
618 pub(crate) sse_timeline: ProjectionAvailabilityV1<SseTimelineMetaV1>,
619}
620
621impl InspectionStructuralProjectionV1 {
622 pub(crate) const fn unsupported() -> Self {
623 Self {
624 json_shape: ProjectionAvailabilityV1::Omitted(
625 ProjectionOmissionReasonV1::UnsupportedFormat,
626 ),
627 sse_timeline: ProjectionAvailabilityV1::Omitted(
628 ProjectionOmissionReasonV1::UnsupportedFormat,
629 ),
630 }
631 }
632
633 pub(crate) const fn unavailable(format: WireFormat) -> Self {
634 match format {
635 WireFormat::Json => Self {
636 json_shape: ProjectionAvailabilityV1::Omitted(
637 ProjectionOmissionReasonV1::ProjectionFailedClosed,
638 ),
639 sse_timeline: ProjectionAvailabilityV1::Omitted(
640 ProjectionOmissionReasonV1::UnsupportedFormat,
641 ),
642 },
643 WireFormat::Sse => Self {
644 json_shape: ProjectionAvailabilityV1::Omitted(
645 ProjectionOmissionReasonV1::UnsupportedFormat,
646 ),
647 sse_timeline: ProjectionAvailabilityV1::Omitted(
648 ProjectionOmissionReasonV1::ProjectionFailedClosed,
649 ),
650 },
651 WireFormat::Ndjson | WireFormat::Utf8Text | WireFormat::Empty => Self {
652 json_shape: ProjectionAvailabilityV1::Omitted(
653 ProjectionOmissionReasonV1::UnsupportedFormat,
654 ),
655 sse_timeline: ProjectionAvailabilityV1::Omitted(
656 ProjectionOmissionReasonV1::UnsupportedFormat,
657 ),
658 },
659 }
660 }
661}
662
663pub(crate) enum InspectionParsedFactsV1 {
669 AnthropicJson(crate::anthropic::JsonInspectionParsedFactsV1),
670 AnthropicSse(crate::anthropic::SseInspectionParsedFactsV1),
671}
672
673impl InspectionParsedFactsV1 {
674 pub(crate) fn project(self) -> InspectionStructuralProjectionV1 {
675 match self {
676 Self::AnthropicJson(facts) => facts.project(),
677 Self::AnthropicSse(facts) => facts.project(),
678 }
679 }
680}
681
682macro_rules! proved_body {
683 ($name:ident) => {
684 pub struct $name {
685 bytes: Vec<u8>,
686 format: WireFormat,
687 provenance: OutputProvenance,
688 inspection_parsed_facts: Option<InspectionParsedFactsV1>,
689 signed_or_encrypted_surface: bool,
690 }
691
692 impl $name {
693 pub(crate) fn new(
694 bytes: Vec<u8>,
695 format: WireFormat,
696 provenance: OutputProvenance,
697 ) -> Self {
698 let signed_or_encrypted_surface = !provenance.signed_opaque_ranges().is_empty();
699 Self {
700 bytes,
701 format,
702 provenance,
703 inspection_parsed_facts: None,
704 signed_or_encrypted_surface,
705 }
706 }
707
708 pub(crate) fn new_with_inspection_parsed_facts(
709 bytes: Vec<u8>,
710 format: WireFormat,
711 provenance: OutputProvenance,
712 inspection_parsed_facts: InspectionParsedFactsV1,
713 ) -> Self {
714 let signed_or_encrypted_surface = !provenance.signed_opaque_ranges().is_empty();
715 Self {
716 bytes,
717 format,
718 provenance,
719 inspection_parsed_facts: (!signed_or_encrypted_surface)
720 .then_some(inspection_parsed_facts),
721 signed_or_encrypted_surface,
722 }
723 }
724
725 #[must_use]
726 pub fn bytes(&self) -> &[u8] {
727 &self.bytes
728 }
729
730 #[must_use]
731 pub const fn format(&self) -> WireFormat {
732 self.format
733 }
734
735 #[must_use]
736 pub const fn provenance(&self) -> &OutputProvenance {
737 &self.provenance
738 }
739
740 pub(crate) const fn signed_or_encrypted_surface(&self) -> bool {
741 self.signed_or_encrypted_surface
742 }
743
744 pub(crate) fn into_inspection_parts(
745 self,
746 ) -> (Vec<u8>, Option<InspectionParsedFactsV1>, bool) {
747 (
748 self.bytes,
749 self.inspection_parsed_facts,
750 self.signed_or_encrypted_surface,
751 )
752 }
753
754 pub(crate) fn take_inspection_parsed_facts(
755 &mut self,
756 ) -> Option<InspectionParsedFactsV1> {
757 self.inspection_parsed_facts.take()
758 }
759
760 #[must_use]
761 pub fn into_bytes(self) -> Vec<u8> {
762 self.bytes
763 }
764 }
765
766 impl fmt::Debug for $name {
767 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
768 formatter.write_str(stringify!($name))
769 }
770 }
771 };
772}
773
774proved_body!(ProvedRequestBody);
775proved_body!(ProvedResponseBody);
776
777pub trait BodyCodec: Send + Sync + 'static {
779 fn protect_request(
780 &self,
781 input: &[u8],
782 ctx: &mut RequestTransformContext<'_>,
783 ) -> Result<ProvedRequestBody, CodecError>;
784
785 fn restore_response(
786 &self,
787 input: &[u8],
788 ctx: &mut ResponseTransformContext<'_>,
789 ) -> Result<ProvedResponseBody, CodecError>;
790}