Skip to main content

abyss_agent_hook/harness/
id.rs

1//! Stable built-in and custom Harness identifiers.
2
3use std::{borrow::Borrow, fmt};
4
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
6
7/// Built-in Harness implementations compiled into the broker.
8#[derive(Clone, Copy, Debug)]
9#[non_exhaustive]
10pub enum BuiltInHarness {
11    /// OpenAI Codex CLI or app traffic.
12    Codex,
13    /// Anthropic Claude Code traffic.
14    ClaudeCode,
15    /// Anthropic Claude Desktop traffic.
16    ClaudeDesktop,
17}
18
19impl BuiltInHarness {
20    /// Returns the stable configuration and event identity.
21    #[must_use]
22    pub const fn id(self) -> &'static str {
23        match self {
24            Self::Codex => "codex",
25            Self::ClaudeCode => "claude-code",
26            Self::ClaudeDesktop => "claude-desktop",
27        }
28    }
29}
30
31/// Stable built-in or custom Harness identity.
32#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
33pub struct HarnessId(String);
34
35impl HarnessId {
36    /// Returns the identifier as carried by configuration and Agent events.
37    #[must_use]
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41
42    /// Returns whether this identifier is reserved for a built-in detector.
43    #[must_use]
44    pub(crate) fn is_reserved(&self) -> bool {
45        matches!(
46            self.as_str(),
47            "codex" | "claude-code" | "claude-desktop" | "openclaw"
48        )
49    }
50}
51
52impl From<BuiltInHarness> for HarnessId {
53    fn from(value: BuiltInHarness) -> Self {
54        Self(value.id().to_owned())
55    }
56}
57
58impl Borrow<str> for HarnessId {
59    fn borrow(&self) -> &str {
60        self.as_str()
61    }
62}
63
64impl fmt::Display for HarnessId {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str(self.as_str())
67    }
68}
69
70impl Serialize for HarnessId {
71    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
72    where
73        S: Serializer,
74    {
75        serializer.serialize_str(self.as_str())
76    }
77}
78
79impl<'de> Deserialize<'de> for HarnessId {
80    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81    where
82        D: Deserializer<'de>,
83    {
84        let value = String::deserialize(deserializer)?;
85        let value = if value == "claude-cli" {
86            BuiltInHarness::ClaudeCode.id().to_owned()
87        } else {
88            value
89        };
90        if value.is_empty()
91            || value.len() > 64
92            || !value.bytes().all(|byte| {
93                byte.is_ascii_lowercase()
94                    || byte.is_ascii_digit()
95                    || matches!(byte, b'.' | b'_' | b'-')
96            })
97        {
98            return Err(D::Error::custom(
99                "Harness id must contain 1-64 lowercase ASCII letters, digits, '.', '_', or '-'",
100            ));
101        }
102        Ok(Self(value))
103    }
104}