Skip to main content

abyss_agent_hook/config/
harness.rs

1//! Harness-specific enablement, content overrides, and custom source matchers.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
6
7use crate::harness::HarnessId;
8
9use super::HarnessUsageContentConfig;
10
11/// Harness usage hook behavior configuration.
12#[derive(Debug, Clone, Default, Serialize)]
13pub struct HarnessUsageConfig {
14    /// Default content policy for all configured Harnesses.
15    #[serde(default)]
16    pub content: HarnessUsageContentConfig,
17    /// Built-in enablement overrides and custom Harness definitions.
18    #[serde(default, alias = "agents")]
19    pub harnesses: BTreeMap<HarnessId, HarnessConfig>,
20}
21
22#[derive(Deserialize)]
23#[serde(deny_unknown_fields)]
24struct HarnessUsageConfigWire {
25    #[serde(default)]
26    content: HarnessUsageContentConfig,
27    #[serde(default, alias = "agents")]
28    harnesses: BTreeMap<HarnessId, HarnessConfig>,
29}
30
31/// Optional policy override and source matchers for one Harness.
32#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33#[serde(deny_unknown_fields)]
34pub struct HarnessConfig {
35    /// Whether this Harness should produce usage events.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub enabled: Option<bool>,
38    /// Content policy override for this Harness.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub content: Option<HarnessUsageContentConfig>,
41    /// Source matchers used by a custom Harness.
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub matchers: Vec<HarnessMatcherConfig>,
44}
45
46/// One custom Harness source matcher.
47#[derive(Debug, Clone, Default, Deserialize, Serialize)]
48#[serde(deny_unknown_fields)]
49pub struct HarnessMatcherConfig {
50    /// Accepted source process names.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub process_names: Vec<String>,
53    /// Accepted platform-neutral application identities.
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub application_ids: Vec<String>,
56}
57
58impl HarnessUsageConfig {
59    /// Returns whether usage events are enabled for one Harness.
60    #[must_use]
61    pub fn enabled_for_harness(&self, harness_id: &str) -> bool {
62        self.harnesses
63            .get(harness_id)
64            .and_then(HarnessConfig::enabled)
65            .unwrap_or(true)
66    }
67
68    /// Returns the effective independent content policy for one Harness.
69    #[must_use]
70    pub fn content_for_harness(&self, harness_id: &str) -> HarnessUsageContentConfig {
71        self.harnesses
72            .get(harness_id)
73            .and_then(HarnessConfig::content)
74            .unwrap_or(&self.content)
75            .clone()
76    }
77}
78
79impl<'de> Deserialize<'de> for HarnessUsageConfig {
80    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81    where
82        D: Deserializer<'de>,
83    {
84        let wire = HarnessUsageConfigWire::deserialize(deserializer)?;
85        for (harness_id, harness) in &wire.harnesses {
86            if harness_id.is_reserved() {
87                if !harness.matchers.is_empty() {
88                    return Err(D::Error::custom(format!(
89                        "built-in Harness `{harness_id}` cannot define custom matchers"
90                    )));
91                }
92                continue;
93            }
94            if harness.matchers.is_empty() {
95                return Err(D::Error::custom(format!(
96                    "custom Harness `{harness_id}` requires at least one matcher"
97                )));
98            }
99            for matcher in &harness.matchers {
100                if matcher.process_names.is_empty() && matcher.application_ids.is_empty() {
101                    return Err(D::Error::custom(format!(
102                        "custom Harness `{harness_id}` contains an empty matcher"
103                    )));
104                }
105                if matcher
106                    .process_names
107                    .iter()
108                    .chain(&matcher.application_ids)
109                    .any(|value| value.trim().is_empty())
110                {
111                    return Err(D::Error::custom(format!(
112                        "custom Harness `{harness_id}` contains an empty matcher value"
113                    )));
114                }
115            }
116        }
117        Ok(Self {
118            content: wire.content,
119            harnesses: wire.harnesses,
120        })
121    }
122}
123
124impl HarnessConfig {
125    const fn enabled(&self) -> Option<bool> {
126        self.enabled
127    }
128
129    const fn content(&self) -> Option<&HarnessUsageContentConfig> {
130        self.content.as_ref()
131    }
132}