systemprompt_models/wire/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]
208 pub const fn bridge_host_id(self) -> Option<&'static str> {
209 match self {
210 Self::ClaudeCode => Some("claude-code"),
211 Self::ClaudeDesktop => Some("claude-desktop"),
212 Self::Codex => Some("codex-cli"),
213 Self::OpenCode => Some("opencode"),
214 Self::Hermes => Some("hermes"),
215 Self::Pi | Self::Other | Self::Internal | Self::Unknown => None,
216 }
217 }
218}
219
220impl InboundWireProtocol {
221 pub const ALL: [Self; 5] = [
222 Self::AnthropicMessages,
223 Self::OpenAiChat,
224 Self::OpenAiResponses,
225 Self::Internal,
226 Self::Unknown,
227 ];
228
229 #[must_use]
230 pub const fn as_str(self) -> &'static str {
231 match self {
232 Self::AnthropicMessages => "anthropic.messages",
233 Self::OpenAiChat => "openai.chat",
234 Self::OpenAiResponses => "openai.responses",
235 Self::Internal => "internal",
236 Self::Unknown => "unknown",
237 }
238 }
239
240 pub fn parse(value: &str) -> Result<Self, OriginParseError> {
241 Self::ALL
242 .into_iter()
243 .find(|wire| wire.as_str() == value)
244 .ok_or_else(|| OriginParseError::WireProtocol(value.to_owned()))
245 }
246}
247
248impl RequestOrigin {
249 pub const INTERNAL: Self = Self {
250 client: ClientKind::Internal,
251 wire: InboundWireProtocol::Internal,
252 attestation: ClientAttestation::Internal,
253 };
254
255 #[must_use]
256 pub const fn gateway(
257 client: ClientKind,
258 wire: InboundWireProtocol,
259 attestation: ClientAttestation,
260 ) -> Self {
261 Self {
262 client,
263 wire,
264 attestation,
265 }
266 }
267}