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