use serde::{de, Deserialize, Deserializer, Serialize};
use thiserror::Error;
pub const MAX_JSON_SHAPE_NODES_V1: usize = 1_024;
pub const MAX_SSE_TIMELINE_ENTRIES_V1: usize = 10_000;
pub const MAX_PII_SUMMARY_ENTRIES_V1: usize = 256;
pub const MAX_DECISION_TRACE_ENTRIES_V1: usize = 256;
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
pub enum InspectionValueErrorV1 {
#[error("invalid_inspection_stage_domain")]
InvalidStageDomain,
#[error("inspection_projection_entry_limit")]
EntryLimit,
#[error("inspection_sequence_overflow")]
SequenceOverflow,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(transparent)]
struct BoundedEntries<T, const MAX: usize>(Vec<T>);
impl<T, const MAX: usize> BoundedEntries<T, MAX> {
const fn empty() -> Self {
Self(Vec::new())
}
fn as_slice(&self) -> &[T] {
self.0.as_slice()
}
}
impl<T, const MAX: usize> TryFrom<Vec<T>> for BoundedEntries<T, MAX> {
type Error = InspectionValueErrorV1;
fn try_from(entries: Vec<T>) -> Result<Self, Self::Error> {
if entries.len() > MAX {
return Err(InspectionValueErrorV1::EntryLimit);
}
Ok(Self(entries))
}
}
impl<'de, T: Deserialize<'de>, const MAX: usize> Deserialize<'de> for BoundedEntries<T, MAX> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct EntriesVisitor<T, const MAX: usize>(std::marker::PhantomData<T>);
struct RejectExcess;
impl<'de> de::DeserializeSeed<'de> for RejectExcess {
type Value = ();
fn deserialize<D: Deserializer<'de>>(self, _deserializer: D) -> Result<(), D::Error> {
Err(de::Error::custom(InspectionValueErrorV1::EntryLimit))
}
}
impl<'de, T: Deserialize<'de>, const MAX: usize> de::Visitor<'de> for EntriesVisitor<T, MAX> {
type Value = BoundedEntries<T, MAX>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "a sequence with at most {MAX} entries")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut entries = Vec::new();
while entries.len() < MAX {
match seq.next_element()? {
Some(entry) => entries.push(entry),
None => return Ok(BoundedEntries(entries)),
}
}
seq.next_element_seed(RejectExcess)?;
Ok(BoundedEntries(entries))
}
}
deserializer.deserialize_seq(EntriesVisitor::<T, MAX>(std::marker::PhantomData))
}
}
macro_rules! ordinal_newtype {
($name:ident, $inner:ty) => {
#[doc = concat!("Bounded provider-neutral `", stringify!($name), "` value.")]
#[derive(
Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(transparent)]
pub struct $name($inner);
impl $name {
#[must_use]
pub const fn new(value: $inner) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> $inner {
self.0
}
}
};
}
ordinal_newtype!(LogicalInspectionIdV1, u64);
ordinal_newtype!(InspectionEmissionIdV1, u64);
ordinal_newtype!(InspectionEpochV1, u64);
ordinal_newtype!(JsonNodeOrdinalV1, u32);
ordinal_newtype!(SseEntryOrdinalV1, u32);
ordinal_newtype!(ContentBlockIndexV1, u32);
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct InspectionSequenceV1(u64);
impl InspectionSequenceV1 {
#[must_use]
pub const fn new(value: u64) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
pub fn checked_next(self) -> Result<Self, InspectionValueErrorV1> {
self.0
.checked_add(1)
.map(Self)
.ok_or(InspectionValueErrorV1::SequenceOverflow)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionStageV1 {
OwnerRequest,
ProviderRequest,
ProviderResponse,
OwnerRestoredResponse,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionDomainV1 {
OwnerRaw,
ProviderVisible,
OwnerRestored,
}
impl InspectionDomainV1 {
pub const ALL: [Self; 3] = [Self::OwnerRaw, Self::ProviderVisible, Self::OwnerRestored];
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionStageDomainV1 {
OwnerRequest,
ProviderRequest,
ProviderResponse,
OwnerRestoredResponse,
}
impl InspectionStageDomainV1 {
#[must_use]
pub const fn stage(self) -> InspectionStageV1 {
match self {
Self::OwnerRequest => InspectionStageV1::OwnerRequest,
Self::ProviderRequest => InspectionStageV1::ProviderRequest,
Self::ProviderResponse => InspectionStageV1::ProviderResponse,
Self::OwnerRestoredResponse => InspectionStageV1::OwnerRestoredResponse,
}
}
#[must_use]
pub const fn domain(self) -> InspectionDomainV1 {
match self {
Self::OwnerRequest => InspectionDomainV1::OwnerRaw,
Self::ProviderRequest | Self::ProviderResponse => InspectionDomainV1::ProviderVisible,
Self::OwnerRestoredResponse => InspectionDomainV1::OwnerRestored,
}
}
pub const fn try_from_parts(
stage: InspectionStageV1,
domain: InspectionDomainV1,
) -> Result<Self, InspectionValueErrorV1> {
match (stage, domain) {
(InspectionStageV1::OwnerRequest, InspectionDomainV1::OwnerRaw) => {
Ok(Self::OwnerRequest)
}
(InspectionStageV1::ProviderRequest, InspectionDomainV1::ProviderVisible) => {
Ok(Self::ProviderRequest)
}
(InspectionStageV1::ProviderResponse, InspectionDomainV1::ProviderVisible) => {
Ok(Self::ProviderResponse)
}
(InspectionStageV1::OwnerRestoredResponse, InspectionDomainV1::OwnerRestored) => {
Ok(Self::OwnerRestoredResponse)
}
_ => Err(InspectionValueErrorV1::InvalidStageDomain),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RouteCodeV1 {
AnthropicMessages,
Unknown,
Rejected,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderProfileCodeV1 {
AnthropicMessagesDirect,
OpenAiCompatible,
GeminiCompatible,
GenericLocalAnthropicCompatible,
OtherReviewed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum EndpointSelectionCodeV1 {
FixedHttpsOrigin,
ConfiguredHttpsOrigin,
OsAssignedLoopback,
DynamicallyDiscoveredLoopback,
RevalidatedLoopback,
UnavailableFailClosed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortSelectionCodeV1 {
NotApplicable,
FixedConfigured,
OsAssigned,
DynamicallyDiscovered,
ChangedAndRevalidated,
UnavailableFailClosed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionOperationalStatusV1 {
Accepted,
Omitted,
Purging,
Disabled,
Closed,
FailedClosed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionErrorCodeV1 {
None,
ProjectionFailedClosed,
SequenceOverflow,
EpochOverflow,
SinkRejected,
SinkPanicked,
RuntimeClosed,
InternalFailClosed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionDropCodeV1 {
None,
QueueFull,
QueueByteLimit,
QueueContended,
QueueClosed,
StaleEpoch,
Purging,
Disabled,
SequenceOverflow,
DuplicateConflict,
PartialEvent,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionDeliveryCodeV1 {
Pending,
Delivered,
Dropped,
SinkRejected,
SinkPanicked,
Closed,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CoarseDurationBucketV1 {
NotMeasured,
UnderOneHundredMilliseconds,
OneHundredMillisecondsToOneSecond,
OneToTenSeconds,
OverTenSeconds,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CaptureDomainsV1 {
MetadataOnly,
OwnerRaw,
ProviderVisible,
OwnerRestored,
OwnerRawAndProviderVisible,
OwnerRawAndOwnerRestored,
ProviderVisibleAndOwnerRestored,
All,
}
impl CaptureDomainsV1 {
pub const ALL: [Self; 8] = [
Self::MetadataOnly,
Self::OwnerRaw,
Self::ProviderVisible,
Self::OwnerRestored,
Self::OwnerRawAndProviderVisible,
Self::OwnerRawAndOwnerRestored,
Self::ProviderVisibleAndOwnerRestored,
Self::All,
];
#[must_use]
pub const fn captures(self, domain: InspectionDomainV1) -> bool {
matches!(
(self, domain),
(Self::OwnerRaw, InspectionDomainV1::OwnerRaw)
| (Self::ProviderVisible, InspectionDomainV1::ProviderVisible)
| (Self::OwnerRestored, InspectionDomainV1::OwnerRestored)
| (
Self::OwnerRawAndProviderVisible,
InspectionDomainV1::OwnerRaw
)
| (
Self::OwnerRawAndProviderVisible,
InspectionDomainV1::ProviderVisible
)
| (Self::OwnerRawAndOwnerRestored, InspectionDomainV1::OwnerRaw)
| (
Self::OwnerRawAndOwnerRestored,
InspectionDomainV1::OwnerRestored
)
| (
Self::ProviderVisibleAndOwnerRestored,
InspectionDomainV1::ProviderVisible
)
| (
Self::ProviderVisibleAndOwnerRestored,
InspectionDomainV1::OwnerRestored
)
| (Self::All, _)
)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct DashboardCaptureDescriptorV1 {
domains: CaptureDomainsV1,
}
impl DashboardCaptureDescriptorV1 {
#[must_use]
pub const fn new(domains: CaptureDomainsV1) -> Self {
Self { domains }
}
#[must_use]
pub const fn domains(self) -> CaptureDomainsV1 {
self.domains
}
#[must_use]
pub const fn captures(self, domain: InspectionDomainV1) -> bool {
self.domains.captures(domain)
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionSurfaceRefV1 {
WholeBody,
JsonNode(JsonNodeOrdinalV1),
SseEntry(SseEntryOrdinalV1),
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionOmissionReasonV1 {
NotCapturedByPolicy,
NotApplicable,
UnsupportedFormat,
MalformedFailClosed,
LimitExceeded,
SignedOrEncryptedSurface,
Purging,
Disabled,
QueueClosed,
ProjectionFailedClosed,
UnknownFuture,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectionAvailabilityV1<T> {
Present(T),
Omitted(ProjectionOmissionReasonV1),
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonTypeCodeV1 {
Object,
Array,
String,
Number,
Boolean,
Null,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct JsonShapeNodeV1 {
ordinal: JsonNodeOrdinalV1,
type_code: JsonTypeCodeV1,
}
impl JsonShapeNodeV1 {
#[must_use]
pub const fn new(ordinal: JsonNodeOrdinalV1, type_code: JsonTypeCodeV1) -> Self {
Self { ordinal, type_code }
}
#[must_use]
pub const fn ordinal(self) -> JsonNodeOrdinalV1 {
self.ordinal
}
#[must_use]
pub const fn type_code(self) -> JsonTypeCodeV1 {
self.type_code
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum JsonShapeTruncationCodeV1 {
Complete,
NodeLimit,
DepthLimit,
UnknownFuture,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct JsonShapeSummaryV1 {
nodes: BoundedEntries<JsonShapeNodeV1, MAX_JSON_SHAPE_NODES_V1>,
truncation: JsonShapeTruncationCodeV1,
}
impl JsonShapeSummaryV1 {
pub fn try_new(
nodes: Vec<JsonShapeNodeV1>,
truncation: JsonShapeTruncationCodeV1,
) -> Result<Self, InspectionValueErrorV1> {
Ok(Self {
nodes: nodes.try_into()?,
truncation,
})
}
#[must_use]
pub fn nodes(&self) -> &[JsonShapeNodeV1] {
self.nodes.as_slice()
}
#[must_use]
pub const fn truncation(&self) -> JsonShapeTruncationCodeV1 {
self.truncation
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SseEventKindV1 {
MessageStart,
ContentBlockStart,
ContentBlockDelta,
ContentBlockStop,
MessageDelta,
MessageStop,
Ping,
Error,
UnknownFuture,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SseDeltaKindV1 {
Text,
InputJson,
Thinking,
Signature,
Citation,
UnknownFuture,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SseTimelineEntryV1 {
ordinal: SseEntryOrdinalV1,
event_kind: SseEventKindV1,
delta_kind: ProjectionAvailabilityV1<SseDeltaKindV1>,
content_block: ProjectionAvailabilityV1<ContentBlockIndexV1>,
}
impl SseTimelineEntryV1 {
#[must_use]
pub const fn new(
ordinal: SseEntryOrdinalV1,
event_kind: SseEventKindV1,
delta_kind: ProjectionAvailabilityV1<SseDeltaKindV1>,
content_block: ProjectionAvailabilityV1<ContentBlockIndexV1>,
) -> Self {
Self {
ordinal,
event_kind,
delta_kind,
content_block,
}
}
#[must_use]
pub const fn ordinal(&self) -> SseEntryOrdinalV1 {
self.ordinal
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SseTimelineMetaV1 {
entries: BoundedEntries<SseTimelineEntryV1, MAX_SSE_TIMELINE_ENTRIES_V1>,
}
impl SseTimelineMetaV1 {
pub fn try_new(entries: Vec<SseTimelineEntryV1>) -> Result<Self, InspectionValueErrorV1> {
Ok(Self {
entries: entries.try_into()?,
})
}
#[must_use]
pub fn entries(&self) -> &[SseTimelineEntryV1] {
self.entries.as_slice()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PiiClassCodeV1 {
PersonName,
Email,
Phone,
Address,
Organization,
Credential,
OtherReviewed,
UnknownFuture,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PiiCountV1 {
class: PiiClassCodeV1,
detected: u32,
pseudonymized: u32,
}
impl PiiCountV1 {
#[must_use]
pub const fn new(class: PiiClassCodeV1, detected: u32, pseudonymized: u32) -> Self {
Self {
class,
detected,
pseudonymized,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PiiSummaryV1 {
entries: BoundedEntries<PiiCountV1, MAX_PII_SUMMARY_ENTRIES_V1>,
}
impl PiiSummaryV1 {
#[must_use]
pub const fn empty() -> Self {
Self {
entries: BoundedEntries::empty(),
}
}
pub fn try_new(entries: Vec<PiiCountV1>) -> Result<Self, InspectionValueErrorV1> {
Ok(Self {
entries: entries.try_into()?,
})
}
#[must_use]
pub fn entries(&self) -> &[PiiCountV1] {
self.entries.as_slice()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionEngineCodeV1 {
DeterministicRecognizer,
Ner,
NeuralSafetyNet,
ResidualScan,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DecisionOutcomeCodeV1 {
RanClean,
RanFindings,
SkippedNotApplicable,
UnavailableFailClosed,
TimeoutFailClosed,
ErrorFailClosed,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DecisionTraceEntryV1 {
engine: DecisionEngineCodeV1,
outcome: DecisionOutcomeCodeV1,
surface: InspectionSurfaceRefV1,
pii_class: ProjectionAvailabilityV1<PiiClassCodeV1>,
detected: u32,
pseudonymized: u32,
}
impl DecisionTraceEntryV1 {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub const fn new(
engine: DecisionEngineCodeV1,
outcome: DecisionOutcomeCodeV1,
surface: InspectionSurfaceRefV1,
pii_class: ProjectionAvailabilityV1<PiiClassCodeV1>,
detected: u32,
pseudonymized: u32,
) -> Self {
Self {
engine,
outcome,
surface,
pii_class,
detected,
pseudonymized,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DecisionTraceV1 {
entries: BoundedEntries<DecisionTraceEntryV1, MAX_DECISION_TRACE_ENTRIES_V1>,
}
impl DecisionTraceV1 {
#[must_use]
pub const fn empty() -> Self {
Self {
entries: BoundedEntries::empty(),
}
}
pub fn try_new(entries: Vec<DecisionTraceEntryV1>) -> Result<Self, InspectionValueErrorV1> {
Ok(Self {
entries: entries.try_into()?,
})
}
#[must_use]
pub fn entries(&self) -> &[DecisionTraceEntryV1] {
self.entries.as_slice()
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AttestationTraceV1 {
NotApplicable,
ExactHit,
MissNoRecord,
MissExpired,
MissEvicted,
InvalidatedContentBytes,
InvalidatedStructuralIdentity,
InvalidatedPolicyOrRulepack,
InvalidatedRecognizerOrArtifact,
InvalidatedLocaleOrDictionary,
InvalidatedNormalizationVersion,
InvalidatedSessionEpoch,
InvalidatedConfigurationGeneration,
AmbiguousFailClosed,
UnavailableFailClosed,
UnknownFuture,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InspectionMeasurementV1 {
byte_count: u64,
chunk_count: u32,
}
impl InspectionMeasurementV1 {
#[must_use]
pub const fn new(byte_count: u64, chunk_count: u32) -> Self {
Self {
byte_count,
chunk_count,
}
}
#[must_use]
pub const fn byte_count(self) -> u64 {
self.byte_count
}
#[must_use]
pub const fn chunk_count(self) -> u32 {
self.chunk_count
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct InspectionQueueSnapshotV1 {
admitted: u64,
dropped: u64,
delivered: u64,
}
impl InspectionQueueSnapshotV1 {
#[must_use]
pub const fn new(admitted: u64, dropped: u64, delivered: u64) -> Self {
Self {
admitted,
dropped,
delivered,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InspectionSafeFieldsV1 {
route: RouteCodeV1,
profile: ProviderProfileCodeV1,
endpoint: EndpointSelectionCodeV1,
port: PortSelectionCodeV1,
status: InspectionOperationalStatusV1,
error: InspectionErrorCodeV1,
drop: InspectionDropCodeV1,
delivery: InspectionDeliveryCodeV1,
duration: CoarseDurationBucketV1,
queue: InspectionQueueSnapshotV1,
}
impl InspectionSafeFieldsV1 {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub const fn new(
route: RouteCodeV1,
profile: ProviderProfileCodeV1,
endpoint: EndpointSelectionCodeV1,
port: PortSelectionCodeV1,
status: InspectionOperationalStatusV1,
error: InspectionErrorCodeV1,
drop: InspectionDropCodeV1,
delivery: InspectionDeliveryCodeV1,
duration: CoarseDurationBucketV1,
queue: InspectionQueueSnapshotV1,
) -> Self {
Self {
route,
profile,
endpoint,
port,
status,
error,
drop,
delivery,
duration,
queue,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum InspectionMetadataVersionV1 {
V1,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct InspectionEventMetaV1 {
version: InspectionMetadataVersionV1,
logical_id: LogicalInspectionIdV1,
emission_id: InspectionEmissionIdV1,
sequence: InspectionSequenceV1,
epoch: InspectionEpochV1,
stage_domain: InspectionStageDomainV1,
route: RouteCodeV1,
profile: ProviderProfileCodeV1,
endpoint: EndpointSelectionCodeV1,
port: PortSelectionCodeV1,
status: InspectionOperationalStatusV1,
error: InspectionErrorCodeV1,
drop: InspectionDropCodeV1,
delivery: InspectionDeliveryCodeV1,
duration: CoarseDurationBucketV1,
queue: InspectionQueueSnapshotV1,
}
impl InspectionEventMetaV1 {
#[must_use]
pub const fn new(
logical_id: LogicalInspectionIdV1,
emission_id: InspectionEmissionIdV1,
sequence: InspectionSequenceV1,
epoch: InspectionEpochV1,
stage_domain: InspectionStageDomainV1,
safe: InspectionSafeFieldsV1,
) -> Self {
Self {
version: InspectionMetadataVersionV1::V1,
logical_id,
emission_id,
sequence,
epoch,
stage_domain,
route: safe.route,
profile: safe.profile,
endpoint: safe.endpoint,
port: safe.port,
status: safe.status,
error: safe.error,
drop: safe.drop,
delivery: safe.delivery,
duration: safe.duration,
queue: safe.queue,
}
}
#[must_use]
pub const fn logical_id(&self) -> LogicalInspectionIdV1 {
self.logical_id
}
#[must_use]
pub const fn emission_id(&self) -> InspectionEmissionIdV1 {
self.emission_id
}
#[must_use]
pub const fn sequence(&self) -> InspectionSequenceV1 {
self.sequence
}
#[must_use]
pub const fn epoch(&self) -> InspectionEpochV1 {
self.epoch
}
#[must_use]
pub const fn stage_domain(&self) -> InspectionStageDomainV1 {
self.stage_domain
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounded_entries_ignore_size_hints_and_stop_at_first_excess_entry() {
struct HintSequence {
entries: std::vec::IntoIter<serde_json::Value>,
hint: Option<usize>,
read: usize,
}
impl<'de> de::SeqAccess<'de> for &mut HintSequence {
type Error = serde_json::Error;
fn next_element_seed<T: de::DeserializeSeed<'de>>(
&mut self,
seed: T,
) -> Result<Option<T::Value>, Self::Error> {
self.read += 1;
self.entries
.next()
.map(|entry| seed.deserialize(entry))
.transpose()
}
fn size_hint(&self) -> Option<usize> {
self.hint
}
}
for hint in [None, Some(0), Some(usize::MAX)] {
for count in [0, 2, 3, 4] {
let mut sequence = HintSequence {
entries: vec![serde_json::json!(7); count].into_iter(),
hint,
read: 0,
};
let result = BoundedEntries::<u8, 2>::deserialize(
de::value::SeqAccessDeserializer::new(&mut sequence),
);
if count > 2 {
assert!(result
.unwrap_err()
.to_string()
.contains("inspection_projection_entry_limit"));
assert_eq!(sequence.read, 3);
assert_eq!(sequence.entries.len(), count - 3);
} else {
assert_eq!(result.unwrap().as_slice(), vec![7; count]);
}
}
}
let error =
serde_json::from_str::<BoundedEntries<u8, 2>>(r#"[1,2,"overflow"]"#).unwrap_err();
assert!(error
.to_string()
.contains("inspection_projection_entry_limit"));
assert!(serde_json::from_str::<BoundedEntries<u8, 0>>("[]").is_ok());
assert!(serde_json::from_str::<BoundedEntries<u8, 0>>("[1]").is_err());
}
macro_rules! projection_cap_test {
($name:ident, $projection:ty, $max:expr, $entry:expr, $wire_entry:literal,
$field:literal, $suffix:literal, $construct:expr, $entries:ident) => {
#[test]
fn $name() {
for count in [0, $max, $max + 1] {
let entries = vec![$entry; count];
let constructed: Result<$projection, InspectionValueErrorV1> =
($construct)(entries);
let wire = format!(
"{{\"{}\":[{}]{}}}",
$field,
vec![$wire_entry; count].join(","),
$suffix,
);
let deserialized = serde_json::from_str::<$projection>(&wire);
if count > $max {
assert_eq!(constructed, Err(InspectionValueErrorV1::EntryLimit));
assert!(deserialized.is_err(), "accepted {count} entries");
assert!(deserialized
.unwrap_err()
.to_string()
.contains("inspection_projection_entry_limit"));
} else {
let constructed = constructed.unwrap();
let deserialized = deserialized.unwrap();
assert_eq!(constructed.$entries().len(), count);
assert_eq!(deserialized, constructed);
assert_eq!(serde_json::to_string(&constructed).unwrap(), wire);
assert_eq!(serde_json::to_string(&deserialized).unwrap(), wire);
}
}
let prefix = format!("{{\"{}\":[{},", $field, vec![$wire_entry; $max].join(","));
for depth in [100_000, 1] {
let wire = format!(
"{prefix}{}0{}]{}}}",
"[".repeat(depth),
"]".repeat(depth),
$suffix,
);
let error = serde_json::from_str::<$projection>(&wire).unwrap_err();
assert!(error
.to_string()
.contains("inspection_projection_entry_limit"));
assert_eq!(error.line(), 1);
assert!(
(prefix.len()..=prefix.len() + 1).contains(&error.column()),
"consumed excess value at depth {depth}: {error}",
);
}
}
};
}
projection_cap_test!(
json_shape_projection_caps_construct_and_deserialize,
JsonShapeSummaryV1,
MAX_JSON_SHAPE_NODES_V1,
JsonShapeNodeV1::new(JsonNodeOrdinalV1::new(0), JsonTypeCodeV1::Null),
r#"{"ordinal":0,"type_code":"null"}"#,
"nodes",
r#","truncation":"complete""#,
|nodes| JsonShapeSummaryV1::try_new(nodes, JsonShapeTruncationCodeV1::Complete),
nodes
);
projection_cap_test!(
sse_timeline_projection_caps_construct_and_deserialize,
SseTimelineMetaV1,
MAX_SSE_TIMELINE_ENTRIES_V1,
SseTimelineEntryV1::new(
SseEntryOrdinalV1::new(0),
SseEventKindV1::ContentBlockDelta,
ProjectionAvailabilityV1::Present(SseDeltaKindV1::Text),
ProjectionAvailabilityV1::Present(ContentBlockIndexV1::new(2)),
),
r#"{"ordinal":0,"event_kind":"content_block_delta","delta_kind":{"present":"text"},"content_block":{"present":2}}"#,
"entries",
"",
SseTimelineMetaV1::try_new,
entries
);
projection_cap_test!(
pii_summary_projection_caps_construct_and_deserialize,
PiiSummaryV1,
MAX_PII_SUMMARY_ENTRIES_V1,
PiiCountV1::new(PiiClassCodeV1::Email, 3, 2),
r#"{"class":"email","detected":3,"pseudonymized":2}"#,
"entries",
"",
PiiSummaryV1::try_new,
entries
);
projection_cap_test!(
decision_trace_projection_caps_construct_and_deserialize,
DecisionTraceV1,
MAX_DECISION_TRACE_ENTRIES_V1,
DecisionTraceEntryV1::new(
DecisionEngineCodeV1::DeterministicRecognizer,
DecisionOutcomeCodeV1::RanFindings,
InspectionSurfaceRefV1::WholeBody,
ProjectionAvailabilityV1::Present(PiiClassCodeV1::Email),
3,
2,
),
r#"{"engine":"deterministic_recognizer","outcome":"ran_findings","surface":"whole_body","pii_class":{"present":"email"},"detected":3,"pseudonymized":2}"#,
"entries",
"",
DecisionTraceV1::try_new,
entries
);
#[test]
fn stage_domain_matrix_is_exhaustive_and_rejects_invalid_pairs() {
let valid = [
InspectionStageDomainV1::OwnerRequest,
InspectionStageDomainV1::ProviderRequest,
InspectionStageDomainV1::ProviderResponse,
InspectionStageDomainV1::OwnerRestoredResponse,
];
assert_eq!(valid.len(), 4);
for pair in valid {
assert_eq!(
InspectionStageDomainV1::try_from_parts(pair.stage(), pair.domain()),
Ok(pair)
);
}
assert!(InspectionStageDomainV1::try_from_parts(
InspectionStageV1::OwnerRequest,
InspectionDomainV1::ProviderVisible,
)
.is_err());
assert_ne!(
InspectionStageDomainV1::ProviderRequest,
InspectionStageDomainV1::ProviderResponse
);
}
#[test]
fn capture_descriptor_covers_all_closed_startup_domain_combinations() {
for domains in CaptureDomainsV1::ALL {
let descriptor = DashboardCaptureDescriptorV1::new(domains);
assert_eq!(descriptor.domains(), domains);
for domain in InspectionDomainV1::ALL {
assert_eq!(descriptor.captures(domain), domains.captures(domain));
}
}
}
#[test]
fn safe_metadata_has_only_closed_operational_fields() {
let metadata = InspectionEventMetaV1::new(
LogicalInspectionIdV1::new(7),
InspectionEmissionIdV1::new(9),
InspectionSequenceV1::new(1),
InspectionEpochV1::new(0),
InspectionStageDomainV1::ProviderRequest,
InspectionSafeFieldsV1::new(
RouteCodeV1::AnthropicMessages,
ProviderProfileCodeV1::AnthropicMessagesDirect,
EndpointSelectionCodeV1::FixedHttpsOrigin,
PortSelectionCodeV1::NotApplicable,
InspectionOperationalStatusV1::Accepted,
InspectionErrorCodeV1::None,
InspectionDropCodeV1::None,
InspectionDeliveryCodeV1::Pending,
CoarseDurationBucketV1::NotMeasured,
InspectionQueueSnapshotV1::default(),
),
);
let value = serde_json::to_value(metadata).unwrap();
let object = value.as_object().unwrap();
let keys: Vec<_> = object.keys().map(String::as_str).collect();
assert_eq!(
keys,
[
"delivery",
"drop",
"duration",
"emission_id",
"endpoint",
"epoch",
"error",
"logical_id",
"port",
"profile",
"queue",
"route",
"sequence",
"stage_domain",
"status",
"version",
]
);
let serialized = serde_json::to_string(&value).unwrap();
for forbidden in [
"payload",
"byte_count",
"chunk_count",
"pii",
"json",
"sse",
"decision",
"attestation",
"hash",
"digest",
"offset",
"path",
"key",
"timestamp",
] {
assert!(
!serialized.contains(forbidden),
"forbidden field {forbidden}"
);
}
}
#[test]
fn json_and_sse_projections_expose_only_the_rev7_allowlists() {
let json = JsonShapeSummaryV1::try_new(
vec![JsonShapeNodeV1::new(
JsonNodeOrdinalV1::new(0),
JsonTypeCodeV1::Object,
)],
JsonShapeTruncationCodeV1::Complete,
)
.unwrap();
let sse = SseTimelineMetaV1::try_new(vec![SseTimelineEntryV1::new(
SseEntryOrdinalV1::new(0),
SseEventKindV1::ContentBlockDelta,
ProjectionAvailabilityV1::Present(SseDeltaKindV1::Text),
ProjectionAvailabilityV1::Present(ContentBlockIndexV1::new(2)),
)])
.unwrap();
assert_eq!(
serde_json::to_value(json).unwrap(),
serde_json::json!({
"nodes": [{"ordinal": 0, "type_code": "object"}],
"truncation": "complete"
})
);
assert_eq!(
serde_json::to_value(sse).unwrap(),
serde_json::json!({
"entries": [{
"ordinal": 0,
"event_kind": "content_block_delta",
"delta_kind": {"present": "text"},
"content_block": {"present": 2}
}]
})
);
}
#[test]
fn projection_availability_is_total_and_sequence_overflow_is_closed() {
let omitted: ProjectionAvailabilityV1<JsonShapeSummaryV1> =
ProjectionAvailabilityV1::Omitted(ProjectionOmissionReasonV1::NotCapturedByPolicy);
assert!(matches!(
omitted,
ProjectionAvailabilityV1::Omitted(ProjectionOmissionReasonV1::NotCapturedByPolicy)
));
assert_eq!(
InspectionSequenceV1::new(u64::MAX).checked_next(),
Err(InspectionValueErrorV1::SequenceOverflow)
);
}
#[test]
fn unknown_codes_reject_and_projection_caps_are_enforced() {
assert!(serde_json::from_str::<RouteCodeV1>("\"caller_route\"").is_err());
assert!(serde_json::from_str::<DecisionEngineCodeV1>("\"model_name\"").is_err());
assert_eq!(
JsonShapeSummaryV1::try_new(
vec![
JsonShapeNodeV1::new(JsonNodeOrdinalV1::new(0), JsonTypeCodeV1::Null,);
MAX_JSON_SHAPE_NODES_V1 + 1
],
JsonShapeTruncationCodeV1::NodeLimit,
),
Err(InspectionValueErrorV1::EntryLimit)
);
}
}