Skip to main content

systemprompt_models/origin/
attestation.rs

1//! The evidence tier behind a `client_kind`, and the body markers that make
2//! up the `native-marker` tier.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use serde::{Deserialize, Serialize};
8
9use super::OriginParseError;
10
11/// How strongly the recorded client is evidenced; see the module head for
12/// the ladder. Ordered strongest-first so `Ord` matches precedence.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum ClientAttestation {
16    HostToken,
17    BridgeSecret,
18    Declared,
19    NativeMarker,
20    UserAgent,
21    None,
22    Internal,
23    Unknown,
24}
25
26/// A structural marker of one harness found in the request body.
27///
28/// Claude Code stamps `metadata.user_id` in one of two grammars —
29/// `user_<hex>_account_<uuid>_session_<uuid>` before 2.1.25x and a JSON
30/// object `{"account_uuid","device_id","session_id"}` from then on — and,
31/// on a third-party gateway, opens the system prompt with
32/// `x-anthropic-billing-header: cc_version=…; cc_entrypoint=<entry>;`. The
33/// entrypoint is the only body signal that tells Claude Desktop (Cowork:
34/// `claude-desktop-3p`, `local-agent`) from the CLI (`cli`), so it outranks
35/// the metadata grammars. Neither grammar was ever sent by `OpenCode`; a
36/// former `opencode-session-json` marker matched the JSON one and mislabelled
37/// every modern Claude Code request.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "kebab-case")]
40pub enum NativeMarker {
41    ClaudeDesktopEntrypoint,
42    ClaudeCliEntrypoint,
43    ClaudeMetadataUserId,
44    ClaudeMetadataJson,
45    CodexTurnMetadata,
46}
47
48impl ClientAttestation {
49    pub const ALL: [Self; 8] = [
50        Self::HostToken,
51        Self::BridgeSecret,
52        Self::Declared,
53        Self::NativeMarker,
54        Self::UserAgent,
55        Self::None,
56        Self::Internal,
57        Self::Unknown,
58    ];
59
60    #[must_use]
61    pub const fn as_str(self) -> &'static str {
62        match self {
63            Self::HostToken => "host-token",
64            Self::BridgeSecret => "bridge-secret",
65            Self::Declared => "declared",
66            Self::NativeMarker => "native-marker",
67            Self::UserAgent => "user-agent",
68            Self::None => "none",
69            Self::Internal => "internal",
70            Self::Unknown => "unknown",
71        }
72    }
73
74    #[must_use]
75    pub const fn label(self) -> &'static str {
76        match self {
77            Self::HostToken => "Attested by bridge host token",
78            Self::BridgeSecret => "Via bridge, host unverified",
79            Self::Declared => "Declared by client",
80            Self::NativeMarker => "Native marker only",
81            Self::UserAgent => "User-Agent only, unverified",
82            Self::None => "No client evidence",
83            Self::Internal => "Internal",
84            Self::Unknown => "Unknown",
85        }
86    }
87
88    pub fn parse(value: &str) -> Result<Self, OriginParseError> {
89        Self::ALL
90            .into_iter()
91            .find(|tier| tier.as_str() == value)
92            .ok_or_else(|| OriginParseError::Attestation(value.to_owned()))
93    }
94
95    #[must_use]
96    pub const fn is_bridge_channel(self) -> bool {
97        matches!(self, Self::HostToken | Self::BridgeSecret)
98    }
99}
100
101impl NativeMarker {
102    pub const ALL: [Self; 5] = [
103        Self::ClaudeDesktopEntrypoint,
104        Self::ClaudeCliEntrypoint,
105        Self::ClaudeMetadataUserId,
106        Self::ClaudeMetadataJson,
107        Self::CodexTurnMetadata,
108    ];
109
110    #[must_use]
111    pub const fn as_str(self) -> &'static str {
112        match self {
113            Self::ClaudeDesktopEntrypoint => "claude-desktop-entrypoint",
114            Self::ClaudeCliEntrypoint => "claude-cli-entrypoint",
115            Self::ClaudeMetadataUserId => "claude-metadata-user-id",
116            Self::ClaudeMetadataJson => "claude-metadata-json",
117            Self::CodexTurnMetadata => "codex-turn-metadata",
118        }
119    }
120
121    pub fn parse(value: &str) -> Result<Self, OriginParseError> {
122        Self::ALL
123            .into_iter()
124            .find(|marker| marker.as_str() == value)
125            .ok_or_else(|| OriginParseError::NativeMarker(value.to_owned()))
126    }
127
128    #[must_use]
129    pub const fn client(self) -> super::ClientKind {
130        match self {
131            Self::ClaudeDesktopEntrypoint => super::ClientKind::ClaudeDesktop,
132            // Why: both metadata.user_id grammars are shared by Claude Code
133            // and Claude Desktop; without an entrypoint the tier records a
134            // marker, not a verified host, and the historical default stands.
135            Self::ClaudeCliEntrypoint | Self::ClaudeMetadataUserId | Self::ClaudeMetadataJson => {
136                super::ClientKind::ClaudeCode
137            },
138            Self::CodexTurnMetadata => super::ClientKind::Codex,
139        }
140    }
141}