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