use std::sync::{Arc, Weak};
use async_trait::async_trait;
use serde_json::Value;
use cpex_core::cmf::MessagePayload;
use cpex_core::context::PluginContext;
use cpex_core::error::{PluginError, PluginViolation};
use cpex_core::executor::ErasedResultFields;
use cpex_core::extensions::Extensions;
use cpex_core::hooks::PluginPayload;
use cpex_core::manager::PluginManager;
use cpex_core::plugin::{Plugin, PluginConfig};
use cpex_core::registry::AnyHookHandler;
use apl_cmf::{extract_args, extract_result, BagBuilder};
use apl_core::evaluator::Decision;
use apl_core::plugin_decl::PluginRegistry;
use apl_core::route::{evaluate_post, evaluate_pre, RoutePayload};
use apl_core::rules::CompiledRoute;
use apl_core::step::PdpResolver;
use crate::cmf_invoker::CmfPluginInvoker;
use crate::delegation_invoker::DelegationPluginInvoker;
use crate::dispatch_plan::DispatchCache;
use crate::pdp_router::PdpRouter;
use crate::session_store::SessionStore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Pre,
Post,
}
pub struct AplRouteHandler {
config: PluginConfig,
route: Arc<CompiledRoute>,
phase: Phase,
plugin_registry: Arc<PluginRegistry>,
dispatch_cache: Arc<DispatchCache>,
session_store: Arc<dyn SessionStore>,
manager: Weak<PluginManager>,
pdp: Arc<dyn PdpResolver>,
}
impl AplRouteHandler {
pub fn new(
config: PluginConfig,
route: Arc<CompiledRoute>,
phase: Phase,
plugin_registry: Arc<PluginRegistry>,
dispatch_cache: Arc<DispatchCache>,
session_store: Arc<dyn SessionStore>,
manager: Weak<PluginManager>,
) -> Self {
Self {
config,
route,
phase,
plugin_registry,
dispatch_cache,
session_store,
manager,
pdp: Arc::new(PdpRouter::new()),
}
}
pub fn with_pdp(mut self, pdp: Arc<dyn PdpResolver>) -> Self {
self.pdp = pdp;
self
}
pub fn with_pdp_router(
mut self,
resolvers: impl IntoIterator<Item = Arc<dyn PdpResolver>>,
) -> Self {
let mut router = PdpRouter::new();
for r in resolvers {
router.register(r);
}
self.pdp = Arc::new(router);
self
}
}
#[async_trait]
impl Plugin for AplRouteHandler {
fn config(&self) -> &PluginConfig {
&self.config
}
}
#[async_trait]
impl AnyHookHandler for AplRouteHandler {
async fn invoke(
&self,
payload: &dyn PluginPayload,
extensions: &Extensions,
_ctx: &mut PluginContext,
) -> Result<Box<dyn std::any::Any + Send + Sync>, Box<PluginError>> {
let msg_payload = payload
.as_any()
.downcast_ref::<MessagePayload>()
.ok_or_else(|| {
Box::new(PluginError::Config {
message: format!(
"AplRouteHandler '{}': payload was not MessagePayload",
self.route.route_key
),
})
})?;
let manager = self.manager.upgrade().ok_or_else(|| {
Box::new(PluginError::Config {
message: format!(
"AplRouteHandler '{}': PluginManager dropped before invoke",
self.route.route_key
),
})
})?;
let plan = self
.dispatch_cache
.get_or_build(&self.route, &self.plugin_registry, &manager)
.await;
let invoker = match CmfPluginInvoker::for_request(
Arc::clone(&manager),
extensions.clone(),
msg_payload.clone(),
plan,
Arc::clone(&self.session_store),
)
.await
{
Ok(inv) => Arc::new(inv),
Err(e) => {
tracing::error!(
alarm = "session_store_failure",
op = "load",
route = %self.route.route_key,
error = %e,
"session label load failed; failing request closed"
);
return Ok(Box::new(ErasedResultFields {
continue_processing: false,
modified_payload: None,
modified_extensions: None,
violation: Some(PluginViolation::new(
"session.load_failed",
"session state could not be loaded",
)),
}));
},
};
let post_extensions = invoker.current_extensions().await;
let mut bag = BagBuilder::new()
.with_extensions(&post_extensions)
.with_route_key(&self.route.route_key)
.build();
let args_value = extract_args_from_message(&msg_payload.message);
let mut route_payload = match self.phase {
Phase::Pre => RoutePayload::new(args_value),
Phase::Post => {
let result_value = extract_result_from_message(&msg_payload.message);
RoutePayload::with_result(args_value, result_value)
},
};
extract_args(&route_payload.args, &mut bag);
if matches!(self.phase, Phase::Post) {
if let Some(result_value) = route_payload.result.as_ref() {
extract_result(result_value, &mut bag);
}
}
let delegations = Arc::new(DelegationPluginInvoker::new(
Arc::clone(&manager),
invoker.extensions_arc(),
invoker.plan_arc(),
));
let invoker_dyn: Arc<dyn apl_core::step::PluginInvoker> = invoker.clone();
let delegations_dyn: Arc<dyn apl_core::step::DelegationInvoker> = delegations.clone();
let decision = match self.phase {
Phase::Pre => {
evaluate_pre(
&self.route,
&mut bag,
&mut route_payload,
&self.pdp,
&invoker_dyn,
&delegations_dyn,
)
.await
},
Phase::Post => {
evaluate_post(
&self.route,
&mut bag,
&mut route_payload,
&self.pdp,
&invoker_dyn,
&delegations_dyn,
)
.await
},
};
invoker.apply_session_taints(&decision.taints).await;
let persist_result = invoker.persist_session().await;
let final_payload = invoker.current_payload().await;
let final_extensions = invoker.current_extensions().await;
let pre_args = extract_args_from_message(&msg_payload.message);
let pre_result = match self.phase {
Phase::Pre => None,
Phase::Post => Some(extract_result_from_message(&msg_payload.message)),
};
let modified_payload: Option<Box<dyn PluginPayload>> = if route_payload.args != pre_args {
let mut updated = final_payload.clone();
write_args_back_to_message(&mut updated.message, &route_payload.args);
Some(Box::new(updated) as Box<dyn PluginPayload>)
} else if matches!(self.phase, Phase::Post)
&& pre_result
.as_ref()
.zip(route_payload.result.as_ref())
.map(|(prev, current)| prev != current)
.unwrap_or(false)
{
let mut updated = final_payload.clone();
if let Some(result_value) = route_payload.result.as_ref() {
write_result_back_to_message(&mut updated.message, result_value);
}
Some(Box::new(updated) as Box<dyn PluginPayload>)
} else if msg_payload.message.get_text_content() != final_payload.message.get_text_content()
{
Some(Box::new(final_payload) as Box<dyn PluginPayload>)
} else {
None
};
let modified_extensions = if extensions_changed(extensions, &final_extensions) {
Some(final_extensions.cow_copy())
} else {
None
};
let (mut continue_processing, mut violation) = match decision.decision {
Decision::Allow => (true, None),
Decision::Deny {
reason,
rule_source,
} => {
let code = if rule_source.is_empty() {
"policy.deny".to_string()
} else {
rule_source
};
let reason = reason.unwrap_or_else(|| "access denied".to_string());
(false, Some(PluginViolation::new(code, reason)))
},
};
if let Err(e) = persist_result {
tracing::error!(
alarm = "session_store_failure",
op = "append",
route = %self.route.route_key,
decision_was_allow = continue_processing,
error = %e,
"session label persist failed; failing request closed"
);
if continue_processing {
continue_processing = false;
violation = Some(PluginViolation::new(
"session.persist_failed",
"session state could not be persisted",
));
}
}
Ok(Box::new(ErasedResultFields {
continue_processing,
modified_payload,
modified_extensions,
violation,
}))
}
fn hook_type_name(&self) -> &'static str {
"cmf"
}
}
fn rewrite_message_text(msg: &mut cpex_core::cmf::Message, new_text: &str) {
for part in msg.content.iter_mut() {
if let cpex_core::cmf::ContentPart::Text { text } = part {
*text = new_text.to_string();
return;
}
}
msg.content.push(cpex_core::cmf::ContentPart::Text {
text: new_text.to_string(),
});
}
fn extract_args_from_message(msg: &cpex_core::cmf::Message) -> Value {
use cpex_core::cmf::ContentPart;
for part in &msg.content {
match part {
ContentPart::ToolCall { content } => {
return Value::Object(
content
.arguments
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
);
},
ContentPart::PromptRequest { content } => {
return Value::Object(
content
.arguments
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
);
},
_ => {},
}
}
Value::String(msg.get_text_content())
}
fn write_args_back_to_message(msg: &mut cpex_core::cmf::Message, args: &Value) {
use cpex_core::cmf::ContentPart;
for part in msg.content.iter_mut() {
match part {
ContentPart::ToolCall { content } => {
if let Some(obj) = args.as_object() {
content.arguments = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
}
return;
},
ContentPart::PromptRequest { content } => {
if let Some(obj) = args.as_object() {
content.arguments = obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
}
return;
},
_ => {},
}
}
if let Some(text) = args.as_str() {
rewrite_message_text(msg, text);
}
}
fn extract_result_from_message(msg: &cpex_core::cmf::Message) -> Value {
use cpex_core::cmf::ContentPart;
for part in &msg.content {
if let ContentPart::ToolResult { content } = part {
return content.content.clone();
}
}
Value::String(msg.get_text_content())
}
fn write_result_back_to_message(msg: &mut cpex_core::cmf::Message, result: &Value) {
use cpex_core::cmf::ContentPart;
for part in msg.content.iter_mut() {
if let ContentPart::ToolResult { content } = part {
content.content = result.clone();
return;
}
}
if let Some(text) = result.as_str() {
rewrite_message_text(msg, text);
}
}
fn extensions_changed(before: &Extensions, after: &Extensions) -> bool {
let security_changed = match (before.security.as_ref(), after.security.as_ref()) {
(Some(a), Some(b)) => !Arc::ptr_eq(a, b),
(None, None) => false,
_ => true,
};
let delegation_changed = match (before.delegation.as_ref(), after.delegation.as_ref()) {
(Some(a), Some(b)) => !Arc::ptr_eq(a, b),
(None, None) => false,
_ => true,
};
let raw_creds_changed = match (
before.raw_credentials.as_ref(),
after.raw_credentials.as_ref(),
) {
(Some(a), Some(b)) => !Arc::ptr_eq(a, b),
(None, None) => false,
_ => true,
};
security_changed || delegation_changed || raw_creds_changed
}