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