1#![forbid(unsafe_code)]
8
9mod redaction;
10mod telemetry_payloads;
11
12use std::{fmt, str::FromStr};
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
16use thiserror::Error;
17
18pub use redaction::{Redacted, redaction_marker, serialize_exposed};
19pub use telemetry_payloads::{set_telemetry_payloads, telemetry_payloads};
20
21const MAX_IDENTIFIER_LENGTH: usize = 253;
22
23#[derive(Clone, Debug, Eq, Error, PartialEq)]
25pub enum IdentifierError {
26 #[error("{kind} identifier must not be empty")]
28 Empty {
29 kind: &'static str,
31 },
32 #[error("{kind} identifier is {length} bytes; the maximum is {maximum}")]
34 TooLong {
35 kind: &'static str,
37 length: usize,
39 maximum: usize,
41 },
42 #[error(
44 "{kind} identifier must start with a lowercase ASCII letter or digit, found {character:?}"
45 )]
46 InvalidStart {
47 kind: &'static str,
49 character: char,
51 },
52 #[error(
54 "{kind} identifier must end with a lowercase ASCII letter or digit, found {character:?}"
55 )]
56 InvalidEnd {
57 kind: &'static str,
59 character: char,
61 },
62 #[error(
64 "{kind} identifier contains invalid character {character:?} at byte {index}; use lowercase ASCII letters, digits, '.', '-', or '_'"
65 )]
66 InvalidCharacter {
67 kind: &'static str,
69 index: usize,
71 character: char,
73 },
74 #[error("{kind} identifier contains adjacent separators at byte {index}")]
76 AdjacentSeparators {
77 kind: &'static str,
79 index: usize,
81 },
82}
83
84fn is_edge_character(character: char) -> bool {
85 character.is_ascii_lowercase() || character.is_ascii_digit()
86}
87
88fn is_separator(character: char) -> bool {
89 matches!(character, '.' | '-' | '_')
90}
91
92fn validate_identifier(value: &str, kind: &'static str) -> Result<(), IdentifierError> {
93 if value.is_empty() {
94 return Err(IdentifierError::Empty { kind });
95 }
96 if value.len() > MAX_IDENTIFIER_LENGTH {
97 return Err(IdentifierError::TooLong {
98 kind,
99 length: value.len(),
100 maximum: MAX_IDENTIFIER_LENGTH,
101 });
102 }
103
104 let mut characters = value.char_indices();
105 let (_, first) = characters.next().ok_or(IdentifierError::Empty { kind })?;
106 if !is_edge_character(first) {
107 return Err(IdentifierError::InvalidStart {
108 kind,
109 character: first,
110 });
111 }
112
113 let mut previous_was_separator = false;
114 for (index, character) in value.char_indices() {
115 if !is_edge_character(character) && !is_separator(character) {
116 return Err(IdentifierError::InvalidCharacter {
117 kind,
118 index,
119 character,
120 });
121 }
122 if is_separator(character) && previous_was_separator {
123 return Err(IdentifierError::AdjacentSeparators { kind, index });
124 }
125 previous_was_separator = is_separator(character);
126 }
127
128 let last = value
129 .chars()
130 .next_back()
131 .ok_or(IdentifierError::Empty { kind })?;
132 if !is_edge_character(last) {
133 return Err(IdentifierError::InvalidEnd {
134 kind,
135 character: last,
136 });
137 }
138
139 Ok(())
140}
141
142macro_rules! identifier {
143 ($name:ident, $label:literal, $docs:literal) => {
144 #[doc = $docs]
145 #[derive(Clone, Debug, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize)]
146 #[serde(transparent)]
147 pub struct $name(String);
148
149 impl $name {
150 #[must_use]
152 pub fn as_str(&self) -> &str {
153 &self.0
154 }
155 }
156
157 impl fmt::Display for $name {
158 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
159 formatter.write_str(&self.0)
160 }
161 }
162
163 impl FromStr for $name {
164 type Err = IdentifierError;
165
166 fn from_str(value: &str) -> Result<Self, Self::Err> {
167 validate_identifier(value, $label)?;
168 Ok(Self(value.to_owned()))
169 }
170 }
171
172 impl TryFrom<String> for $name {
173 type Error = IdentifierError;
174
175 fn try_from(value: String) -> Result<Self, Self::Error> {
176 validate_identifier(&value, $label)?;
177 Ok(Self(value))
178 }
179 }
180
181 impl TryFrom<&str> for $name {
182 type Error = IdentifierError;
183
184 fn try_from(value: &str) -> Result<Self, Self::Error> {
185 value.parse()
186 }
187 }
188
189 impl From<$name> for String {
190 fn from(value: $name) -> Self {
191 value.0
192 }
193 }
194
195 impl<'de> Deserialize<'de> for $name {
196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197 where
198 D: Deserializer<'de>,
199 {
200 let value = String::deserialize(deserializer)?;
201 value.parse().map_err(D::Error::custom)
202 }
203 }
204 };
205}
206
207identifier!(AgentId, "agent", "A validated agent resource identifier.");
208identifier!(
209 CapabilityId,
210 "capability",
211 "A validated capability resource identifier."
212);
213identifier!(
214 ProviderId,
215 "provider",
216 "A validated capability-provider identifier."
217);
218identifier!(TaskId, "task", "A validated task identifier.");
219identifier!(
220 InvocationId,
221 "invocation",
222 "A validated capability invocation identifier."
223);
224identifier!(TraceId, "trace", "A validated end-to-end trace identifier.");
225identifier!(
226 PrincipalId,
227 "principal",
228 "A validated authenticated principal identifier."
229);
230
231#[derive(Clone, Debug, Deserialize, Eq, Hash, JsonSchema, PartialEq, Serialize)]
233#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
234pub enum Actor {
235 Human {
237 principal: PrincipalId,
239 },
240 Agent {
242 agent: AgentId,
244 },
245 Service {
247 principal: PrincipalId,
249 },
250}
251
252#[derive(
254 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
255)]
256#[serde(rename_all = "PascalCase")]
257pub enum RiskLevel {
258 Low,
260 Medium,
262 High,
264 Critical,
266}
267
268impl fmt::Display for RiskLevel {
269 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(formatter, "{self:?}")
271 }
272}
273
274#[derive(
276 Clone, Copy, Debug, Deserialize, Eq, Hash, JsonSchema, Ord, PartialEq, PartialOrd, Serialize,
277)]
278#[serde(rename_all = "PascalCase")]
279pub enum AgentStatus {
280 Disabled,
282 Pending,
284 Ready,
286 Error,
288}
289
290impl fmt::Display for AgentStatus {
291 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
292 write!(formatter, "{self:?}")
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::{AgentId, IdentifierError, RiskLevel};
299
300 #[test]
301 fn accepts_portable_identifiers() {
302 for value in ["reviewer", "github.pull-request.read", "agent_2"] {
303 let parsed = value.parse::<AgentId>();
304 assert!(parsed.is_ok(), "{value} should be valid: {parsed:?}");
305 }
306 }
307
308 #[test]
309 fn rejects_invalid_identifiers_with_context() {
310 assert!(matches!(
311 "Reviewer".parse::<AgentId>(),
312 Err(IdentifierError::InvalidStart { .. })
313 ));
314 assert!(matches!(
315 "github..read".parse::<AgentId>(),
316 Err(IdentifierError::AdjacentSeparators { .. })
317 ));
318 assert!(matches!(
319 "reviewer/one".parse::<AgentId>(),
320 Err(IdentifierError::InvalidCharacter { index: 8, .. })
321 ));
322 assert!(matches!(
323 "reviewer-".parse::<AgentId>(),
324 Err(IdentifierError::InvalidEnd { .. })
325 ));
326 }
327
328 #[test]
329 fn deserialization_cannot_bypass_validation() {
330 let error = serde_json::from_str::<AgentId>(r#""not valid""#)
331 .expect_err("whitespace must be rejected");
332 assert!(error.to_string().contains("invalid character"));
333 }
334
335 #[test]
336 fn display_is_stable() {
337 assert_eq!(RiskLevel::High.to_string(), "High");
338 }
339}