mod a2a;
pub mod identity;
use std::sync::LazyLock;
use serde_json::json;
use systemprompt_identifiers::{AgentName, ContextId, SessionId, TraceId};
use systemprompt_runtime::AppContext;
use systemprompt_security::authz::{AuthzContext, AuthzDecision, AuthzRequest, EntityRef};
use a2a::{authenticated_user, build_a2a_request, mint_a2a_token, run_agent};
use identity::resolve_or_link_user;
static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
#[must_use]
pub fn http_client() -> reqwest::Client {
CLIENT.clone()
}
#[derive(Debug, Clone)]
pub enum ReplyTarget {
Channel { id: String },
Url { url: String },
}
#[derive(Debug, Clone)]
pub struct MessagingInbound {
pub platform: &'static str,
pub issuer: String,
pub org_id: String,
pub channel_id: String,
pub external_user_id: String,
pub text: String,
pub agent_name: AgentName,
pub entity: EntityRef,
pub reply: ReplyTarget,
}
#[derive(Debug, Clone)]
pub enum DispatchOutcome {
Replied(String),
Denied(String),
}
#[derive(Debug, thiserror::Error)]
pub enum MessagingError {
#[error("identity resolution failed: {0}")]
Identity(String),
#[error("token minting failed: {0}")]
Token(String),
#[error("agent dispatch failed: {0}")]
Dispatch(String),
#[error("malformed agent response: {0}")]
Response(String),
}
pub async fn dispatch_messaging(
ctx: &AppContext,
inbound: MessagingInbound,
) -> Result<DispatchOutcome, MessagingError> {
let user = resolve_or_link_user(ctx, &inbound.issuer, &inbound.external_user_id).await?;
let authed = authenticated_user(&user)?;
let authz = AuthzRequest {
entity: inbound.entity.clone(),
user_id: user.id.clone(),
roles: user.roles.clone(),
attributes: std::collections::BTreeMap::new(),
trace_id: TraceId::generate(),
session_id: None,
context: AuthzContext::extension(
format!("{}.message", inbound.platform),
json!({ "channel": inbound.channel_id }),
),
act_chain: Vec::new(),
};
if let AuthzDecision::Deny { reason, policy } = ctx.authz_hook().evaluate(authz).await {
return Ok(DispatchOutcome::Denied(format!("{policy}: {reason}")));
}
let context_id =
ContextId::derived_from_messaging(inbound.platform, &inbound.org_id, &inbound.channel_id);
let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
let token = mint_a2a_token(ctx, &authed, &session_id)?;
let request = build_a2a_request(&inbound, &authed, &session_id, &token, &context_id)?;
let reply = run_agent(ctx, inbound.agent_name.as_str(), request).await?;
Ok(DispatchOutcome::Replied(reply))
}
#[cfg(feature = "test-api")]
pub mod test_api {
use systemprompt_agent::models::a2a::Task;
#[must_use]
pub fn reply_text(task: Option<&Task>) -> String {
super::a2a::reply_text(task)
}
}