use crate::{AssertedConversationVisibility, Attribution, ExternalIdentity, TurnMessage};
use polyc_proto::proto::polychrome::agent::v1::{
IngressSourceIdentity as WireIngressSourceIdentity, ingress_source_identity,
};
pub const MAX_CLAIMED_NAMESPACE_BYTES: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ClaimedNamespaceError {
#[error("a claimed conversation namespace must not be empty")]
Empty,
#[error(
"a claimed conversation namespace is 1..={MAX_CLAIMED_NAMESPACE_BYTES} bytes, got {actual}"
)]
TooLong {
actual: usize,
},
#[error("a claimed conversation namespace holds lowercase ASCII letters and digits only")]
InvalidCharacter,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ClaimedNamespace(String);
impl ClaimedNamespace {
pub fn new(value: impl Into<String>) -> Result<Self, ClaimedNamespaceError> {
let value = value.into();
if value.is_empty() {
return Err(ClaimedNamespaceError::Empty);
}
if value.len() > MAX_CLAIMED_NAMESPACE_BYTES {
return Err(ClaimedNamespaceError::TooLong {
actual: value.len(),
});
}
if !value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
{
return Err(ClaimedNamespaceError::InvalidCharacter);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ClaimedNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IngressIdentityError {
#[error("ingress source namespace must not be empty")]
EmptyNamespace,
#[error("reported ingress event id must not be empty")]
EmptyReportedId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct IngressIdentity {
namespace: String,
event: IngressEventId,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum IngressEventId {
Reported(String),
Derived([u8; 32]),
}
impl IngressIdentity {
pub fn reported(
namespace: impl Into<String>,
event_id: impl Into<String>,
) -> Result<Self, IngressIdentityError> {
let namespace = namespace.into();
if namespace.is_empty() {
return Err(IngressIdentityError::EmptyNamespace);
}
let event_id = event_id.into();
if event_id.is_empty() {
return Err(IngressIdentityError::EmptyReportedId);
}
Ok(Self {
namespace,
event: IngressEventId::Reported(event_id),
})
}
pub fn reported_components(
namespace: impl Into<String>,
components: &[&str],
) -> Result<Self, IngressIdentityError> {
if components.is_empty() || components.iter().any(|part| part.is_empty()) {
return Err(IngressIdentityError::EmptyReportedId);
}
let mut framed = String::new();
for part in components {
framed.push_str(&part.len().to_string());
framed.push(':');
framed.push_str(part);
framed.push('/');
}
Self::reported(namespace, framed)
}
pub fn derived(
namespace: impl Into<String>,
authenticated_fields_digest: [u8; 32],
) -> Result<Self, IngressIdentityError> {
let namespace = namespace.into();
if namespace.is_empty() {
return Err(IngressIdentityError::EmptyNamespace);
}
Ok(Self {
namespace,
event: IngressEventId::Derived(authenticated_fields_digest),
})
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
pub(crate) fn to_wire(&self) -> WireIngressSourceIdentity {
let event_id = match &self.event {
IngressEventId::Reported(id) => {
ingress_source_identity::EventId::ReportedId(id.clone())
}
IngressEventId::Derived(digest) => {
ingress_source_identity::EventId::DerivedDigest(digest.to_vec())
}
};
WireIngressSourceIdentity {
namespace: self.namespace.clone(),
event_id: Some(event_id),
..Default::default()
}
}
}
pub use polyc_proto::proto::polychrome::agent::v1::Priority;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct IngressDirective {
pub budget_cap: Option<u32>,
pub priority: Option<Priority>,
pub required_approver: Option<ExternalIdentity>,
}
impl IngressDirective {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.budget_cap.is_none() && self.priority.is_none() && self.required_approver.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConversationIdOrigin {
Minted,
AuthenticatedSource,
CallerSupplied,
}
pub trait EdgeAdapter {
type Native;
type Inbound;
fn namespace(&self) -> &'static str;
#[must_use]
fn claimed_namespace(&self) -> ClaimedNamespace {
ClaimedNamespace::new(self.namespace()).expect("an edge's namespace is a valid claim")
}
fn conversation_id(&self, native: &Self::Native) -> String;
fn to_turn_input(&self, inbound: &Self::Inbound) -> Vec<TurnMessage>;
fn caller(&self, _inbound: &Self::Inbound) -> Option<ExternalIdentity> {
None
}
fn ingress_directive(&self, _inbound: &Self::Inbound) -> IngressDirective {
IngressDirective::default()
}
}
pub fn build_attribution<E: EdgeAdapter>(
edge: &E,
trigger: Option<&E::Inbound>,
observed: &[E::Inbound],
visibility: AssertedConversationVisibility,
) -> Attribution {
fn same(a: &ExternalIdentity, b: &ExternalIdentity) -> bool {
a.provider == b.provider && a.scope == b.scope && a.external_id == b.external_id
}
let caller = trigger.and_then(|t| edge.caller(t));
let mut participants: Vec<ExternalIdentity> = Vec::new();
for unit in observed {
let Some(identity) = edge.caller(unit) else {
continue;
};
let duplicate = caller.as_ref().is_some_and(|c| same(c, &identity))
|| participants.iter().any(|p| same(p, &identity));
if !duplicate {
participants.push(identity);
}
}
Attribution {
caller,
participants,
conversation_visibility: visibility,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use crate::{attributed_message, hashed_conversation_id};
#[test]
fn every_real_namespace_is_a_valid_claim() {
for namespace in [
"slack", "telegram", "discord", "email", "evt", "a2a", "web", "app", "mcp", "routine",
"eval", "cli",
] {
assert_eq!(
ClaimedNamespace::new(namespace)
.expect("a real namespace is a valid claim")
.as_str(),
namespace,
);
}
}
#[test]
fn the_wildcard_is_not_a_valid_claim() {
assert_eq!(
ClaimedNamespace::new("*"),
Err(ClaimedNamespaceError::InvalidCharacter),
);
}
#[test]
fn an_empty_claim_is_refused() {
assert_eq!(ClaimedNamespace::new(""), Err(ClaimedNamespaceError::Empty));
}
#[test]
fn a_claim_holding_a_colon_is_refused() {
assert_eq!(
ClaimedNamespace::new("slack:team"),
Err(ClaimedNamespaceError::InvalidCharacter),
);
}
#[test]
fn a_claim_is_lowercase_ascii_only() {
for rejected in ["Slack", "web-chat", "web_chat", "café", "web.chat", " web"] {
assert_eq!(
ClaimedNamespace::new(rejected),
Err(ClaimedNamespaceError::InvalidCharacter),
"{rejected} must not be a valid claim",
);
}
}
#[test]
fn a_claim_is_bounded() {
let longest = "a".repeat(MAX_CLAIMED_NAMESPACE_BYTES);
assert!(ClaimedNamespace::new(longest).is_ok());
let over = "a".repeat(MAX_CLAIMED_NAMESPACE_BYTES + 1);
assert_eq!(
ClaimedNamespace::new(over),
Err(ClaimedNamespaceError::TooLong {
actual: MAX_CLAIMED_NAMESPACE_BYTES + 1,
}),
);
}
#[test]
fn source_identity_is_stable_when_execution_identity_changes() {
let identity = crate::IngressIdentity::reported("workspace-1", "message-42")
.expect("reported source identity is valid");
let first = identity.to_wire();
let second = identity.to_wire();
assert_eq!(first, second);
assert_eq!(identity.namespace(), "workspace-1");
}
#[test]
fn composite_reported_identity_is_injective() {
let left =
IngressIdentity::reported_components("source", &["a/b", "c"]).expect("valid identity");
let right =
IngressIdentity::reported_components("source", &["a", "b/c"]).expect("valid identity");
assert_ne!(left, right);
}
struct ExampleEdge {
namespace_uuid: uuid::Uuid,
}
struct Thread {
team: String,
channel: String,
thread_ts: String,
}
struct Line {
speaker: String,
text: String,
}
impl EdgeAdapter for ExampleEdge {
type Native = Thread;
type Inbound = Line;
fn namespace(&self) -> &'static str {
"example"
}
fn conversation_id(&self, native: &Thread) -> String {
hashed_conversation_id(
self.namespace_uuid,
&[&native.team, &native.channel, &native.thread_ts],
)
}
fn to_turn_input(&self, inbound: &Line) -> Vec<TurnMessage> {
if inbound.text.trim().is_empty() {
return Vec::new();
}
vec![attributed_message(&inbound.speaker, &inbound.text)]
}
}
#[test]
fn conversation_id_is_stable_per_native_unit() {
let edge = ExampleEdge {
namespace_uuid: uuid::Uuid::from_u128(0x42),
};
let t = Thread {
team: "T1".to_owned(),
channel: "C1".to_owned(),
thread_ts: "169.0".to_owned(),
};
assert_eq!(edge.conversation_id(&t), edge.conversation_id(&t));
assert_eq!(edge.namespace(), "example");
}
#[test]
fn ingress_drops_empty_and_attributes_speakers() {
let edge = ExampleEdge {
namespace_uuid: uuid::Uuid::from_u128(0x42),
};
assert!(
edge.to_turn_input(&Line {
speaker: "Alice".to_owned(),
text: " ".to_owned(),
})
.is_empty()
);
assert_eq!(
edge.to_turn_input(&Line {
speaker: "Alice".to_owned(),
text: "hi".to_owned(),
}),
vec![attributed_message("Alice", "hi")]
);
}
}