use crate::auth_lys::session::LysSession;
use crate::auth_lys::{AccessAction, AccessResource, ResourceKind};
use crate::auth_lys::{HotAction, SubjectBinding};
use crate::config::templates::is_segment;
use super::error::AgentError;
#[must_use]
pub fn subject(subject: &str) -> Option<(&str, &str)> {
let rest = subject.strip_prefix("agents.")?;
let (name, leaf) = rest.split_once('.')?;
(is_segment(name)
&& matches!(
leaf,
"inbox" | "presence" | "state" | "stream" | "plan" | "cancel"
))
.then_some((name, leaf))
}
#[must_use]
pub fn server_owned(value: &str) -> bool {
subject(value).is_some_and(|(_, leaf)| matches!(leaf, "presence" | "state"))
}
pub fn refuse_server_publish(value: &str) -> Result<(), AgentError> {
if server_owned(value) {
Err(AgentError::ServerOwned(value.into()))
} else {
Ok(())
}
}
pub fn evaluate_subject<'a>(
pass: &'a lys_pass::VerifiedPass,
resource: &AccessResource,
action: AccessAction,
now: u64,
) -> Result<lys_pass::Decision<'a>, lys_pass::Error> {
if let Some((name, action)) = agent_target(resource, action) {
return pass.evaluate("liminal.agent", name, action, now);
}
pass.evaluate(resource.kind.qualified(), &resource.id, action.name(), now)
}
#[must_use]
pub fn refusal(
resource: &AccessResource,
action: AccessAction,
request: &str,
cause: crate::auth_lys::RefusalCause,
) -> crate::auth_lys::AccessDecisionError {
let refusal = if let Some((name, action)) = agent_target(resource, action) {
crate::auth_lys::AccessRefusal::for_target(
"liminal",
"liminal.agent",
name,
action,
request.into(),
)
} else {
crate::auth_lys::AccessRefusal::new(resource.clone(), action, request.into())
};
crate::auth_lys::AccessDecisionError { cause, refusal }
}
fn agent_target(resource: &AccessResource, action: AccessAction) -> Option<(&str, &'static str)> {
if resource.kind != ResourceKind::Subject {
return None;
}
let (name, leaf) = subject(&resource.id)?;
let action = match (action, leaf) {
(AccessAction::Publish, "inbox" | "cancel") => "message",
(AccessAction::Subscribe, "presence" | "state" | "stream") => "watch",
_ => return None,
};
Some((name, action))
}
pub fn claim(session: Option<&dyn LysSession>, name: &str) -> Result<(), AgentError> {
if session
.is_some_and(|session| {
matches!(session.holder_kind(), Some("agent" | "person")) && session.holder() == name
})
{
return Ok(());
}
Err(AgentError::Identity {
holder: session.map_or("unverified", LysSession::holder).into(),
agent: name.into(),
})
}
pub fn check(
session: Option<&dyn LysSession>,
action: HotAction,
value: &str,
) -> Result<(), AgentError> {
let Some((name, leaf)) = subject(value) else {
if value.starts_with("agents.")
&& value.split('.').count() != 3
&& !matches!(
value,
"agents.session"
| "agents.permission"
| "agents.refuse_prompt"
| "agents.session_close"
)
{
return Err(AgentError::Invalid("agent subject"));
}
return Ok(());
};
if (action == HotAction::Subscribe && leaf == "inbox")
|| (matches!(action, HotAction::Publish | HotAction::Serve)
&& !matches!(leaf, "inbox" | "cancel"))
{
claim(session, name)?;
}
if matches!(action, HotAction::Publish | HotAction::Serve) && server_owned(value) {
return Err(AgentError::ServerOwned(value.into()));
}
if session.is_none() {
return Err(AgentError::Invalid("agent subjects require a Lys pass"));
}
Ok(())
}
pub fn decide(
session: Option<&dyn LysSession>,
network: &str,
action: HotAction,
value: &str,
at_ms: u64,
) -> Result<(), AgentError> {
check(session, action, value)?;
let session = session.ok_or(AgentError::Invalid("agent subjects require a Lys pass"))?;
let binding = SubjectBinding::new(network.into(), value.into(), value.into())
.map_err(|_| AgentError::Invalid("agent subject binding"))?;
session
.decide(&binding, action, at_ms / 1000)
.map_err(|error| AgentError::Store(error.to_string()))?;
Ok(())
}
#[must_use]
pub fn is_host_route(subject: &str) -> bool {
matches!(
subject,
"agents.session" | "agents.permission" | "agents.refuse_prompt" | "agents.session_close"
)
}