Skip to main content

systemprompt_identifiers/
agent.rs

1//! Agent identity newtypes: opaque [`AgentId`] (UUID-backed), checked
2//! [`AgentName`] (non-empty, at most 64 bytes of ASCII alphanumerics and
3//! `-_.:/`, rejects the `"unknown"`/`"unset"` sentinels), and
4//! [`ExternalAgentId`] for off-platform "super-agents" (Claude Desktop,
5//! Codex CLI, Claude Code) that connect via the bridge binary.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10crate::define_id!(AgentId, generate, schema);
11crate::define_id!(ExternalAgentId, non_empty);
12
13use crate::error::IdValidationError;
14
15crate::define_id!(AgentName, checked, validate_agent_name);
16
17const MAX_AGENT_NAME_BYTES: usize = 64;
18
19fn validate_agent_name(name: &str) -> Result<(), IdValidationError> {
20    if name.trim().is_empty() {
21        return Err(IdValidationError::empty("AgentName"));
22    }
23    if name.len() > MAX_AGENT_NAME_BYTES {
24        return Err(IdValidationError::invalid(
25            "AgentName",
26            format!("longer than {MAX_AGENT_NAME_BYTES} bytes"),
27        ));
28    }
29    if !name.chars().all(allowed_agent_name_char) {
30        return Err(IdValidationError::invalid(
31            "AgentName",
32            "only ASCII letters, digits and '-', '_', '.', ':', '/' are allowed",
33        ));
34    }
35    if name.eq_ignore_ascii_case("unknown") || name.eq_ignore_ascii_case("unset") {
36        return Err(IdValidationError::invalid(
37            "AgentName",
38            format!("'{name}' is a reserved sentinel, not an agent name"),
39        ));
40    }
41    Ok(())
42}
43
44const fn allowed_agent_name_char(c: char) -> bool {
45    c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':' | '/')
46}
47
48impl AgentName {
49    pub fn system() -> Self {
50        Self("system".to_owned())
51    }
52
53    pub fn bridge() -> Self {
54        Self("bridge".to_owned())
55    }
56}