Skip to main content

systemprompt_models/wire/origin/
evidence.rs

1//! Everything the wire carried about the client, kept beside the
2//! classification so it can be audited or re-derived.
3//!
4//! Every string is truncated to its `ai_request_client_evidence` column bound
5//! at construction, so an insert can never trip the length CHECK.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use serde::{Deserialize, Serialize};
11
12use super::{ClientAttestation, ClientKind, NativeMarker};
13
14pub(super) const DECLARED_CLIENT_MAX: usize = 64;
15pub(super) const UA_PRODUCT_MAX: usize = 64;
16pub(super) const UA_VERSION_MAX: usize = 64;
17pub(super) const SDK_FIELD_MAX: usize = 64;
18
19/// Which tier named `client_kind`, and what the wire carried.
20///
21/// `kind_source` is the tier that named `client_kind`; on the `bridge-secret`
22/// channel it is one of the lower tiers, since the secret says nothing about
23/// the host. Nullable fields mean "not presented", never "empty".
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct ClientEvidence {
26    pub kind_source: ClientAttestation,
27    pub attested_host: Option<ClientKind>,
28    pub declared_client: Option<String>,
29    pub native_marker: Option<NativeMarker>,
30    pub ua_product: Option<String>,
31    pub ua_version: Option<String>,
32    pub sdk_lang: Option<String>,
33    pub sdk_package_version: Option<String>,
34    pub sdk_runtime: Option<String>,
35    pub sdk_runtime_version: Option<String>,
36    pub sdk_os: Option<String>,
37    pub sdk_arch: Option<String>,
38}
39
40impl ClientEvidence {
41    #[must_use]
42    pub const fn none() -> Self {
43        Self {
44            kind_source: ClientAttestation::None,
45            attested_host: None,
46            declared_client: None,
47            native_marker: None,
48            ua_product: None,
49            ua_version: None,
50            sdk_lang: None,
51            sdk_package_version: None,
52            sdk_runtime: None,
53            sdk_runtime_version: None,
54            sdk_os: None,
55            sdk_arch: None,
56        }
57    }
58
59    #[must_use]
60    pub const fn internal() -> Self {
61        let mut evidence = Self::none();
62        evidence.kind_source = ClientAttestation::Internal;
63        evidence
64    }
65}
66
67#[must_use]
68pub(super) fn bounded(value: Option<&str>, max: usize) -> Option<String> {
69    let value = value?.trim();
70    if value.is_empty() {
71        return None;
72    }
73    let mut end = value.len().min(max);
74    while !value.is_char_boundary(end) {
75        end -= 1;
76    }
77    Some(value[..end].to_owned())
78}