agent-runtime-types 0.1.0

Internal value types for Agent Runtime
Documentation
//! Stable identifiers shared across Runtime Domain boundaries.
//!
//! Identifiers validate at construction so empty or unbounded strings cannot
//! enter domain state. Transport and provider types do not belong here.

use std::{fmt, str::FromStr, sync::Arc};

use serde::{Deserialize, Serialize};

macro_rules! identifier {
    ($name:ident, $label:literal) => {
        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
        #[serde(transparent)]
        pub struct $name(String);

        impl $name {
            pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
                let value = value.into();
                validate($label, &value)?;
                Ok(Self(value))
            }

            pub fn as_str(&self) -> &str {
                &self.0
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                self.0.fmt(formatter)
            }
        }

        impl FromStr for $name {
            type Err = IdentifierError;

            fn from_str(value: &str) -> Result<Self, Self::Err> {
                Self::new(value)
            }
        }
    };
}

identifier!(ExecutionId, "execution_id");
identifier!(ConversationId, "conversation_id");
identifier!(RuntimeInstanceId, "runtime_instance_id");
identifier!(WorkspaceId, "workspace_id");
identifier!(ToolCallId, "tool_call_id");
identifier!(OperationId, "operation_id");
identifier!(EventId, "event_id");
identifier!(PackageId, "package_id");

/// Opaque execution delegation reference. It is durable but cannot directly
/// authenticate a request; Debug remains redacted because it is recovery data.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DelegationLeaseRef(String);

impl DelegationLeaseRef {
    pub fn new(value: impl Into<String>) -> Result<Self, IdentifierError> {
        let value = value.into();
        if !value.starts_with("edl_") {
            return Err(IdentifierError {
                label: "delegation_lease_ref",
                reason: "must be an opaque edl_ reference",
            });
        }
        validate("delegation_lease_ref", &value)?;
        Ok(Self(value))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for DelegationLeaseRef {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("DelegationLeaseRef([REDACTED OPAQUE REFERENCE])")
    }
}

impl fmt::Display for DelegationLeaseRef {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("[REDACTED OPAQUE REFERENCE]")
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CallerScope {
    pub subject: String,
    pub tenant_id: String,
    pub project_id: String,
    #[serde(default)]
    pub capabilities: Vec<String>,
}

impl CallerScope {
    pub fn validate(&self) -> Result<(), IdentifierError> {
        validate("subject", &self.subject)?;
        validate("tenant_id", &self.tenant_id)?;
        validate("project_id", &self.project_id)?;
        Ok(())
    }
}

/// Transient caller credential. It is intentionally not serializable and its
/// Debug representation is always redacted.
#[derive(Clone)]
pub struct CredentialHandle(Arc<str>);

impl CredentialHandle {
    pub fn new(value: impl Into<Arc<str>>) -> Result<Self, IdentifierError> {
        let value = value.into();
        if value.is_empty() || value.len() > 16 * 1024 || value.chars().any(char::is_control) {
            return Err(IdentifierError {
                label: "credential",
                reason: "must contain 1..=16384 non-control bytes",
            });
        }
        Ok(Self(value))
    }

    pub fn expose(&self) -> &str {
        &self.0
    }
}

impl fmt::Debug for CredentialHandle {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("CredentialHandle([REDACTED])")
    }
}

#[derive(Clone, Debug)]
pub struct RequestAuthority {
    pub caller: CallerScope,
    pub credential: CredentialHandle,
}

impl ExecutionId {
    pub fn random() -> Self {
        Self(uuid::Uuid::new_v4().to_string())
    }
}

impl OperationId {
    pub fn random() -> Self {
        Self(uuid::Uuid::new_v4().to_string())
    }
}

impl EventId {
    pub fn random() -> Self {
        Self(uuid::Uuid::new_v4().to_string())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifierError {
    label: &'static str,
    reason: &'static str,
}

impl fmt::Display for IdentifierError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{} {}", self.label, self.reason)
    }
}

impl std::error::Error for IdentifierError {}

fn validate(label: &'static str, value: &str) -> Result<(), IdentifierError> {
    if value.is_empty() {
        return Err(IdentifierError {
            label,
            reason: "must not be empty",
        });
    }
    if value.len() > 256 {
        return Err(IdentifierError {
            label,
            reason: "must not exceed 256 bytes",
        });
    }
    if value.chars().any(char::is_control) {
        return Err(IdentifierError {
            label,
            reason: "must not contain control characters",
        });
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn identifiers_reject_empty_and_control_characters() {
        assert!(ExecutionId::new("").is_err());
        assert!(ExecutionId::new("bad\nvalue").is_err());
        assert_eq!(
            ExecutionId::new("execution-1").unwrap().as_str(),
            "execution-1"
        );
    }
}