systemprompt_models/origin/
mod.rs1mod attestation;
46mod classify;
47mod evidence;
48mod harness;
49
50pub use attestation::{ClientAttestation, NativeMarker};
51pub use classify::{
52 ClassificationInput, ClassificationRejection, Classified, StainlessHeaders, classify,
53 native_marker, ua_product,
54};
55pub use evidence::ClientEvidence;
56
57use serde::{Deserialize, Serialize};
58
59use crate::bridge::host::{HostAppKind, HostKind, HostProduct};
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case")]
70pub enum ClientKind {
71 ClaudeCode,
72 ClaudeDesktop,
73 Codex,
74 #[serde(rename = "opencode")]
75 OpenCode,
76 Hermes,
77 Pi,
78 Other,
79 Internal,
80 Unknown,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86pub enum InboundWireProtocol {
87 #[serde(rename = "anthropic.messages")]
88 AnthropicMessages,
89 #[serde(rename = "openai.chat")]
90 OpenAiChat,
91 #[serde(rename = "openai.responses")]
92 OpenAiResponses,
93 #[serde(rename = "internal")]
94 Internal,
95 #[serde(rename = "unknown")]
96 Unknown,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102pub struct RequestOrigin {
103 pub client: ClientKind,
104 pub wire: InboundWireProtocol,
105 pub attestation: ClientAttestation,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
109pub enum OriginParseError {
110 #[error("unknown client kind: {0}")]
111 ClientKind(String),
112 #[error("unknown inbound wire protocol: {0}")]
113 WireProtocol(String),
114 #[error("unknown client attestation: {0}")]
115 Attestation(String),
116 #[error("unknown native marker: {0}")]
117 NativeMarker(String),
118 #[error("client kind {0} is not an evaluator harness")]
119 NotAHarness(&'static str),
120}
121
122impl ClientKind {
123 pub const ALL: [Self; 9] = [
124 Self::ClaudeCode,
125 Self::ClaudeDesktop,
126 Self::Codex,
127 Self::OpenCode,
128 Self::Hermes,
129 Self::Pi,
130 Self::Other,
131 Self::Internal,
132 Self::Unknown,
133 ];
134
135 pub const DECLARABLE: [Self; 7] = [
138 Self::ClaudeCode,
139 Self::ClaudeDesktop,
140 Self::Codex,
141 Self::OpenCode,
142 Self::Hermes,
143 Self::Pi,
144 Self::Other,
145 ];
146
147 #[must_use]
148 pub const fn as_str(self) -> &'static str {
149 match self {
150 Self::ClaudeCode => "claude-code",
151 Self::ClaudeDesktop => "claude-desktop",
152 Self::Codex => "codex",
153 Self::OpenCode => "opencode",
154 Self::Hermes => "hermes",
155 Self::Pi => "pi",
156 Self::Other => "other",
157 Self::Internal => "internal",
158 Self::Unknown => "unknown",
159 }
160 }
161
162 #[must_use]
163 pub const fn label(self) -> &'static str {
164 match self {
165 Self::ClaudeCode => "Claude Code",
166 Self::ClaudeDesktop => "Claude Desktop",
167 Self::Codex => "Codex",
168 Self::OpenCode => "OpenCode",
169 Self::Hermes => "Hermes",
170 Self::Pi => "Pi",
171 Self::Other => "API client",
172 Self::Internal => "Internal",
173 Self::Unknown => "Unknown",
174 }
175 }
176
177 pub fn parse(value: &str) -> Result<Self, OriginParseError> {
178 Self::ALL
179 .into_iter()
180 .find(|kind| kind.as_str() == value)
181 .ok_or_else(|| OriginParseError::ClientKind(value.to_owned()))
182 }
183
184 pub fn hosts(self) -> impl Iterator<Item = HostKind> {
185 HostKind::ALL
186 .into_iter()
187 .filter(move |host| Self::from(*host) == self)
188 }
189
190 #[must_use]
191 pub const fn product(self) -> Option<HostProduct> {
192 match self {
193 Self::ClaudeCode | Self::ClaudeDesktop => Some(HostProduct::Claude),
194 Self::Codex => Some(HostProduct::Codex),
195 Self::OpenCode => Some(HostProduct::OpenCode),
196 Self::Hermes => Some(HostProduct::Hermes),
197 Self::Pi | Self::Other | Self::Internal | Self::Unknown => None,
198 }
199 }
200
201 #[must_use]
204 pub fn from_ua_product(product: &str) -> Option<Self> {
205 match product {
206 "claude-cli" | "claude-code" => Some(Self::ClaudeCode),
207 "claude-desktop" => Some(Self::ClaudeDesktop),
208 "codex_cli_rs" => Some(Self::Codex),
209 "opencode" => Some(Self::OpenCode),
210 "hermes-agent" => Some(Self::Hermes),
211 _ => None,
212 }
213 }
214
215 #[must_use]
220 pub const fn same_runtime(self, other: Self) -> bool {
221 (self as u8) == (other as u8)
222 || matches!(
223 (self.product(), other.product()),
224 (Some(HostProduct::Claude), Some(HostProduct::Claude))
225 )
226 }
227}
228
229impl From<HostKind> for ClientKind {
230 fn from(host: HostKind) -> Self {
231 match host.product() {
232 HostProduct::Claude => match host.kind() {
233 HostAppKind::DesktopApp => Self::ClaudeDesktop,
234 HostAppKind::CliTool => Self::ClaudeCode,
235 },
236 HostProduct::Codex => Self::Codex,
237 HostProduct::OpenCode => Self::OpenCode,
238 HostProduct::Hermes => Self::Hermes,
239 }
240 }
241}
242
243impl InboundWireProtocol {
244 pub const ALL: [Self; 5] = [
245 Self::AnthropicMessages,
246 Self::OpenAiChat,
247 Self::OpenAiResponses,
248 Self::Internal,
249 Self::Unknown,
250 ];
251
252 #[must_use]
253 pub const fn as_str(self) -> &'static str {
254 match self {
255 Self::AnthropicMessages => "anthropic.messages",
256 Self::OpenAiChat => "openai.chat",
257 Self::OpenAiResponses => "openai.responses",
258 Self::Internal => "internal",
259 Self::Unknown => "unknown",
260 }
261 }
262
263 pub fn parse(value: &str) -> Result<Self, OriginParseError> {
264 Self::ALL
265 .into_iter()
266 .find(|wire| wire.as_str() == value)
267 .ok_or_else(|| OriginParseError::WireProtocol(value.to_owned()))
268 }
269}
270
271impl RequestOrigin {
272 pub const INTERNAL: Self = Self {
273 client: ClientKind::Internal,
274 wire: InboundWireProtocol::Internal,
275 attestation: ClientAttestation::Internal,
276 };
277
278 #[must_use]
279 pub const fn gateway(
280 client: ClientKind,
281 wire: InboundWireProtocol,
282 attestation: ClientAttestation,
283 ) -> Self {
284 Self {
285 client,
286 wire,
287 attestation,
288 }
289 }
290}