Skip to main content

systemprompt_models/wire/origin/
attestation.rs

1//! The evidence tier behind a `client_kind`, and the body markers that make
2//! up the `native-marker` tier.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use serde::{Deserialize, Serialize};
8
9use super::OriginParseError;
10
11/// How strongly the recorded client is evidenced; see the module head for
12/// the ladder. Ordered strongest-first so `Ord` matches precedence.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum ClientAttestation {
16    HostToken,
17    BridgeSecret,
18    Declared,
19    NativeMarker,
20    UserAgent,
21    None,
22    Internal,
23    Unknown,
24}
25
26/// A structural marker of one harness found in the request body.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum NativeMarker {
30    ClaudeMetadataUserId,
31    CodexTurnMetadata,
32    OpencodeSessionJson,
33}
34
35impl ClientAttestation {
36    pub const ALL: [Self; 8] = [
37        Self::HostToken,
38        Self::BridgeSecret,
39        Self::Declared,
40        Self::NativeMarker,
41        Self::UserAgent,
42        Self::None,
43        Self::Internal,
44        Self::Unknown,
45    ];
46
47    #[must_use]
48    pub const fn as_str(self) -> &'static str {
49        match self {
50            Self::HostToken => "host-token",
51            Self::BridgeSecret => "bridge-secret",
52            Self::Declared => "declared",
53            Self::NativeMarker => "native-marker",
54            Self::UserAgent => "user-agent",
55            Self::None => "none",
56            Self::Internal => "internal",
57            Self::Unknown => "unknown",
58        }
59    }
60
61    #[must_use]
62    pub const fn label(self) -> &'static str {
63        match self {
64            Self::HostToken => "Attested by bridge host token",
65            Self::BridgeSecret => "Via bridge, host unverified",
66            Self::Declared => "Declared by client",
67            Self::NativeMarker => "Native marker only",
68            Self::UserAgent => "User-Agent only, unverified",
69            Self::None => "No client evidence",
70            Self::Internal => "Internal",
71            Self::Unknown => "Unknown",
72        }
73    }
74
75    pub fn parse(value: &str) -> Result<Self, OriginParseError> {
76        Self::ALL
77            .into_iter()
78            .find(|tier| tier.as_str() == value)
79            .ok_or_else(|| OriginParseError::Attestation(value.to_owned()))
80    }
81
82    #[must_use]
83    pub const fn is_bridge_channel(self) -> bool {
84        matches!(self, Self::HostToken | Self::BridgeSecret)
85    }
86}
87
88impl NativeMarker {
89    pub const ALL: [Self; 3] = [
90        Self::ClaudeMetadataUserId,
91        Self::CodexTurnMetadata,
92        Self::OpencodeSessionJson,
93    ];
94
95    #[must_use]
96    pub const fn as_str(self) -> &'static str {
97        match self {
98            Self::ClaudeMetadataUserId => "claude-metadata-user-id",
99            Self::CodexTurnMetadata => "codex-turn-metadata",
100            Self::OpencodeSessionJson => "opencode-session-json",
101        }
102    }
103
104    pub fn parse(value: &str) -> Result<Self, OriginParseError> {
105        Self::ALL
106            .into_iter()
107            .find(|marker| marker.as_str() == value)
108            .ok_or_else(|| OriginParseError::NativeMarker(value.to_owned()))
109    }
110
111    #[must_use]
112    pub const fn client(self) -> super::ClientKind {
113        match self {
114            // Why: the Claude metadata.user_id grammar is shared by Claude
115            // Code and Claude Desktop; the tier records that it is a marker,
116            // not a verified host, and the historical default stands.
117            Self::ClaudeMetadataUserId => super::ClientKind::ClaudeCode,
118            Self::CodexTurnMetadata => super::ClientKind::Codex,
119            Self::OpencodeSessionJson => super::ClientKind::OpenCode,
120        }
121    }
122}