abyss_agent_hook/config/
harness.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
6
7use crate::harness::HarnessId;
8
9use super::HarnessUsageContentConfig;
10
11#[derive(Debug, Clone, Default, Serialize)]
13pub struct HarnessUsageConfig {
14 #[serde(default)]
16 pub content: HarnessUsageContentConfig,
17 #[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#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33#[serde(deny_unknown_fields)]
34pub struct HarnessConfig {
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub enabled: Option<bool>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub content: Option<HarnessUsageContentConfig>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub matchers: Vec<HarnessMatcherConfig>,
44}
45
46#[derive(Debug, Clone, Default, Deserialize, Serialize)]
48#[serde(deny_unknown_fields)]
49pub struct HarnessMatcherConfig {
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub process_names: Vec<String>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub application_ids: Vec<String>,
56}
57
58impl HarnessUsageConfig {
59 #[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 #[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}