use std::fmt;
use serde::{Deserialize, Serialize};
use crate::UserId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Actor {
pub user_id: UserId,
pub kind: ActorKind,
}
impl Actor {
#[must_use]
pub const fn user(user_id: UserId) -> Self {
Self {
user_id,
kind: ActorKind::User,
}
}
#[must_use]
pub const fn anonymous(user_id: UserId) -> Self {
Self {
user_id,
kind: ActorKind::Anonymous,
}
}
#[must_use]
pub const fn system(user_id: UserId) -> Self {
Self {
user_id,
kind: ActorKind::System,
}
}
#[must_use]
pub fn job(user_id: UserId, job_name: impl Into<String>) -> Self {
Self {
user_id,
kind: ActorKind::Job {
job_name: job_name.into(),
},
}
}
#[must_use]
pub fn mcp(user_id: UserId, server_name: impl Into<String>) -> Self {
Self {
user_id,
kind: ActorKind::Mcp {
server_name: server_name.into(),
},
}
}
#[must_use]
pub fn agent(user_id: UserId, agent_id: impl Into<String>) -> Self {
Self {
user_id,
kind: ActorKind::Agent {
agent_id: agent_id.into(),
},
}
}
#[must_use]
pub fn audit_columns(&self) -> (&str, &str) {
(self.kind.as_str(), self.kind.actor_id(&self.user_id))
}
#[must_use]
pub fn from_tool_name(user_id: UserId, agent_id: Option<&str>, tool_name: &str) -> Self {
if let Some(rest) = tool_name.strip_prefix("mcp__") {
if let Some(server) = rest.split("__").next() {
if !server.is_empty() {
return Self::mcp(user_id, server);
}
}
}
match agent_id {
Some(id) if !id.is_empty() => Self::agent(user_id, id),
_ => Self::user(user_id),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ActorKind {
User,
Anonymous,
System,
Job { job_name: String },
Mcp { server_name: String },
Agent { agent_id: String },
}
impl ActorKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::User => "user",
Self::Anonymous => "anonymous",
Self::System => "system",
Self::Job { .. } => "job",
Self::Mcp { .. } => "mcp",
Self::Agent { .. } => "agent",
}
}
#[must_use]
pub fn actor_id<'a>(&'a self, user_id: &'a UserId) -> &'a str {
match self {
Self::User | Self::Anonymous | Self::System => user_id.as_str(),
Self::Job { job_name } => job_name.as_str(),
Self::Mcp { server_name } => server_name.as_str(),
Self::Agent { agent_id } => agent_id.as_str(),
}
}
}
impl ActorKind {
#[must_use]
pub const fn tag(&self) -> ActorKindTag {
match self {
Self::User => ActorKindTag::User,
Self::Anonymous => ActorKindTag::Anonymous,
Self::System => ActorKindTag::System,
Self::Job { .. } => ActorKindTag::Job,
Self::Mcp { .. } => ActorKindTag::Mcp,
Self::Agent { .. } => ActorKindTag::Agent,
}
}
}
impl fmt::Display for ActorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(type_name = "TEXT", rename_all = "snake_case"))]
#[serde(rename_all = "snake_case")]
pub enum ActorKindTag {
User,
Anonymous,
System,
Job,
Mcp,
Agent,
}
impl ActorKindTag {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::User => "user",
Self::Anonymous => "anonymous",
Self::System => "system",
Self::Job => "job",
Self::Mcp => "mcp",
Self::Agent => "agent",
}
}
}
impl fmt::Display for ActorKindTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}