use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use cpex_core::elicitation::{
ElicitationHook, ElicitationOp, ElicitationOutcomeKind, ElicitationPayload,
ElicitationStatusKind,
};
use cpex_core::hooks::payload::Extensions;
use cpex_core::manager::PluginManager;
use apl_core::step::{
ElicitStep, ElicitationDispatch, ElicitationError, ElicitationInvoker, ElicitationOutcome,
ElicitationStatus, ElicitationValidation,
};
use crate::dispatch_plan::RouteDispatchPlan;
pub struct ElicitationPluginInvoker {
manager: Arc<PluginManager>,
extensions: Arc<Mutex<Extensions>>,
plan: Arc<RouteDispatchPlan>,
}
impl ElicitationPluginInvoker {
pub fn new(
manager: Arc<PluginManager>,
extensions: Arc<Mutex<Extensions>>,
plan: Arc<RouteDispatchPlan>,
) -> Self {
Self {
manager,
extensions,
plan,
}
}
fn entry_for(
&self,
plugin_name: &str,
) -> Result<cpex_core::registry::HookEntry, ElicitationError> {
self.plan
.elicitation_entries
.get(plugin_name)
.cloned()
.ok_or_else(|| ElicitationError::NotFound(plugin_name.to_string()))
}
async fn invoke(
&self,
op: &str,
plugin_name: &str,
payload: ElicitationPayload,
) -> Result<ElicitationPayload, ElicitationError> {
let entry = self.entry_for(plugin_name)?;
let current_extensions = self.extensions.lock().await.clone();
let (result, _bg) = self
.manager
.invoke_entries::<ElicitationHook>(
std::slice::from_ref(&entry),
payload,
current_extensions,
None,
)
.await;
if !result.continue_processing {
let detail = result
.violation
.map(|v| format!("{}: {}", v.code, v.reason))
.unwrap_or_else(|| "denied without violation detail".to_string());
return Err(ElicitationError::Handler(format!(
"{op}: plugin `{plugin_name}` halted: {detail}"
)));
}
ElicitationPayload::from_pipeline_result(&result).ok_or_else(|| {
ElicitationError::Handler(format!(
"{op}: plugin `{plugin_name}` returned allow but no ElicitationPayload"
))
})
}
}
fn apply_step_inputs(mut payload: ElicitationPayload, step: &ElicitStep) -> ElicitationPayload {
if let Some(purpose) = &step.purpose {
payload = payload.with_purpose(purpose.clone());
}
if let Some(scope) = &step.scope {
payload = payload.with_scope(scope.clone());
}
if let Some(timeout) = &step.timeout {
payload = payload.with_timeout(timeout.clone());
}
if let Some(channel) = &step.channel {
payload = payload.with_channel(channel.clone());
}
payload
}
#[async_trait]
impl ElicitationInvoker for ElicitationPluginInvoker {
async fn dispatch(
&self,
step: &ElicitStep,
resolved_from: &str,
) -> Result<ElicitationDispatch, ElicitationError> {
let payload = apply_step_inputs(
ElicitationPayload::new(ElicitationOp::Dispatch, step.kind.as_str(), resolved_from),
step,
);
let out = self.invoke("dispatch", &step.plugin_name, payload).await?;
let id = out.id.ok_or_else(|| {
ElicitationError::Handler(format!(
"dispatch: plugin `{}` dispatched without returning an id",
step.plugin_name
))
})?;
Ok(ElicitationDispatch {
id,
approver: out.approver,
intent_id: out.intent_id,
expires_at: out.expires_at,
})
}
async fn check(
&self,
step: &ElicitStep,
id: &str,
) -> Result<ElicitationStatus, ElicitationError> {
let payload = apply_step_inputs(
ElicitationPayload::new(ElicitationOp::Check, step.kind.as_str(), "")
.with_elicitation_id(id),
step,
);
let out = self.invoke("check", &step.plugin_name, payload).await?;
match out.status {
Some(ElicitationStatusKind::Pending) | None => Ok(ElicitationStatus::Pending),
Some(ElicitationStatusKind::Expired) => Ok(ElicitationStatus::Expired),
Some(ElicitationStatusKind::Resolved) => {
let outcome = match out.outcome {
Some(ElicitationOutcomeKind::Approved) => ElicitationOutcome::Approved,
Some(ElicitationOutcomeKind::Denied) | None => ElicitationOutcome::Denied,
};
Ok(ElicitationStatus::Resolved { outcome })
},
}
}
async fn validate(
&self,
step: &ElicitStep,
id: &str,
) -> Result<ElicitationValidation, ElicitationError> {
let payload = apply_step_inputs(
ElicitationPayload::new(ElicitationOp::Validate, step.kind.as_str(), "")
.with_elicitation_id(id),
step,
);
let out = self.invoke("validate", &step.plugin_name, payload).await?;
Ok(ElicitationValidation {
valid: out.valid.unwrap_or(false),
approver: out.approver,
intent_id: out.intent_id,
reason: out.reason,
})
}
}