mod attestation;
mod classify;
mod evidence;
mod harness;
pub use attestation::{ClientAttestation, NativeMarker};
pub use classify::{
ClassificationInput, ClassificationRejection, Classified, StainlessHeaders, classify,
native_marker, ua_product,
};
pub use evidence::ClientEvidence;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClientKind {
ClaudeCode,
ClaudeDesktop,
Codex,
#[serde(rename = "opencode")]
OpenCode,
Hermes,
Pi,
Other,
Internal,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum InboundWireProtocol {
#[serde(rename = "anthropic.messages")]
AnthropicMessages,
#[serde(rename = "openai.chat")]
OpenAiChat,
#[serde(rename = "openai.responses")]
OpenAiResponses,
#[serde(rename = "internal")]
Internal,
#[serde(rename = "unknown")]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RequestOrigin {
pub client: ClientKind,
pub wire: InboundWireProtocol,
pub attestation: ClientAttestation,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OriginParseError {
#[error("unknown client kind: {0}")]
ClientKind(String),
#[error("unknown inbound wire protocol: {0}")]
WireProtocol(String),
#[error("unknown client attestation: {0}")]
Attestation(String),
#[error("unknown native marker: {0}")]
NativeMarker(String),
#[error("client kind {0} is not an evaluator harness")]
NotAHarness(&'static str),
}
impl ClientKind {
pub const ALL: [Self; 9] = [
Self::ClaudeCode,
Self::ClaudeDesktop,
Self::Codex,
Self::OpenCode,
Self::Hermes,
Self::Pi,
Self::Other,
Self::Internal,
Self::Unknown,
];
pub const DECLARABLE: [Self; 7] = [
Self::ClaudeCode,
Self::ClaudeDesktop,
Self::Codex,
Self::OpenCode,
Self::Hermes,
Self::Pi,
Self::Other,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ClaudeCode => "claude-code",
Self::ClaudeDesktop => "claude-desktop",
Self::Codex => "codex",
Self::OpenCode => "opencode",
Self::Hermes => "hermes",
Self::Pi => "pi",
Self::Other => "other",
Self::Internal => "internal",
Self::Unknown => "unknown",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::ClaudeCode => "Claude Code",
Self::ClaudeDesktop => "Claude Desktop",
Self::Codex => "Codex",
Self::OpenCode => "OpenCode",
Self::Hermes => "Hermes",
Self::Pi => "Pi",
Self::Other => "API client",
Self::Internal => "Internal",
Self::Unknown => "Unknown",
}
}
pub fn parse(value: &str) -> Result<Self, OriginParseError> {
Self::ALL
.into_iter()
.find(|kind| kind.as_str() == value)
.ok_or_else(|| OriginParseError::ClientKind(value.to_owned()))
}
#[must_use]
pub fn from_bridge_host_id(host_id: &str) -> Option<Self> {
match host_id {
"claude-code" => Some(Self::ClaudeCode),
"claude-desktop" => Some(Self::ClaudeDesktop),
"codex-cli" => Some(Self::Codex),
"opencode" => Some(Self::OpenCode),
"hermes" => Some(Self::Hermes),
_ => None,
}
}
#[must_use]
pub fn from_ua_product(product: &str) -> Option<Self> {
match product {
"claude-cli" | "claude-code" => Some(Self::ClaudeCode),
"claude-desktop" => Some(Self::ClaudeDesktop),
"codex_cli_rs" => Some(Self::Codex),
"opencode" => Some(Self::OpenCode),
"hermes-agent" => Some(Self::Hermes),
_ => None,
}
}
#[must_use]
pub const fn bridge_host_id(self) -> Option<&'static str> {
match self {
Self::ClaudeCode => Some("claude-code"),
Self::ClaudeDesktop => Some("claude-desktop"),
Self::Codex => Some("codex-cli"),
Self::OpenCode => Some("opencode"),
Self::Hermes => Some("hermes"),
Self::Pi | Self::Other | Self::Internal | Self::Unknown => None,
}
}
}
impl InboundWireProtocol {
pub const ALL: [Self; 5] = [
Self::AnthropicMessages,
Self::OpenAiChat,
Self::OpenAiResponses,
Self::Internal,
Self::Unknown,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::AnthropicMessages => "anthropic.messages",
Self::OpenAiChat => "openai.chat",
Self::OpenAiResponses => "openai.responses",
Self::Internal => "internal",
Self::Unknown => "unknown",
}
}
pub fn parse(value: &str) -> Result<Self, OriginParseError> {
Self::ALL
.into_iter()
.find(|wire| wire.as_str() == value)
.ok_or_else(|| OriginParseError::WireProtocol(value.to_owned()))
}
}
impl RequestOrigin {
pub const INTERNAL: Self = Self {
client: ClientKind::Internal,
wire: InboundWireProtocol::Internal,
attestation: ClientAttestation::Internal,
};
#[must_use]
pub const fn gateway(
client: ClientKind,
wire: InboundWireProtocol,
attestation: ClientAttestation,
) -> Self {
Self {
client,
wire,
attestation,
}
}
}