#![deny(unsafe_code)]
#![warn(missing_docs, rust_2018_idioms)]
pub mod audit;
pub use pack_verbs::{eval_pack_function, eval_pack_function_cached};
pub mod operations;
pub mod pack_verbs;
pub mod preflight;
pub use exocortex_storage::{Direction, Invalidation, TraversalSpec, VisibilityContext};
use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{de::DeserializeOwned, Serialize};
#[derive(Clone)]
pub struct OpContext {
pub visibility_ctx: exocortex_storage::VisibilityContext,
pub audit_admin: bool,
pub storage: std::sync::Arc<dyn exocortex_storage::Storage>,
pub cache: std::sync::Arc<exocortex_cache::LocalCache>,
pub deadline: chrono::DateTime<chrono::Utc>,
pub ontology: Option<std::sync::Arc<exocortex_kernel::Ontology>>,
pub ingest_preflight: Option<std::sync::Arc<dyn IngestPreflight>>,
}
#[async_trait]
pub trait IngestPreflight: Send + Sync {
async fn preflight_signed(
&self,
principal: &VisibilityContext,
batch: exocortex_wire::ingest::v1::IngestBatch,
) -> Result<exocortex_wire::ingest::v1::IngestAck, OpError>;
}
impl OpContext {
pub fn per_request(
visibility_ctx: exocortex_storage::VisibilityContext,
storage: std::sync::Arc<dyn exocortex_storage::Storage>,
cache: std::sync::Arc<exocortex_cache::LocalCache>,
budget: chrono::Duration,
) -> Self {
Self {
visibility_ctx,
audit_admin: false,
storage,
cache,
deadline: chrono::Utc::now() + budget,
ontology: None,
ingest_preflight: None,
}
}
pub fn with_audit_admin(mut self, audit_admin: bool) -> Self {
self.audit_admin = audit_admin;
self
}
pub fn with_ontology(mut self, ontology: std::sync::Arc<exocortex_kernel::Ontology>) -> Self {
self.ontology = Some(ontology);
self
}
pub fn with_ingest_preflight(mut self, handle: std::sync::Arc<dyn IngestPreflight>) -> Self {
self.ingest_preflight = Some(handle);
self
}
pub fn check_deadline(&self) -> Result<(), OpError> {
if chrono::Utc::now() > self.deadline {
Err(OpError::DeadlineExceeded)
} else {
Ok(())
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum OpError {
#[error("bad input: {0}")]
BadInput(String),
#[error("unauthorized: {0}")]
Unauthorized(String),
#[error("not found")]
NotFound,
#[error("deadline exceeded")]
DeadlineExceeded,
#[error("storage: {0}")]
Storage(String),
#[error("{0}")]
Other(String),
}
#[async_trait]
pub trait Operation: Send + Sync + 'static {
type Input: DeserializeOwned + JsonSchema + Send;
type Output: Serialize + JsonSchema + Send;
fn name(&self) -> &'static str;
fn mcp_tool_name(&self) -> &'static str;
fn http_method(&self) -> http::Method;
fn http_path(&self) -> &'static str;
async fn handle(&self, ctx: &OpContext, input: Self::Input) -> Result<Self::Output, OpError>;
}
pub struct OperationEntry {
pub name: &'static str,
pub mcp_tool_name: &'static str,
pub pack: Option<&'static str>,
pub http_method: fn() -> http::Method,
pub http_path: &'static str,
pub input_schema: fn() -> schemars::schema::RootSchema,
pub output_schema: fn() -> schemars::schema::RootSchema,
#[allow(clippy::type_complexity)]
pub handler: for<'a> fn(
&'static OperationEntry,
&'a OpContext,
serde_json::Value,
)
-> futures::future::BoxFuture<'a, Result<serde_json::Value, OpError>>,
}
inventory::collect!(OperationEntry);
#[macro_export]
macro_rules! register_operation {
($op:ident, $name:literal, $mcp:literal, $method:ident, $path:literal, $input:ty, $output:ty) => {
impl $crate::OperationNames for $op {
const NAME_OVERRIDE: &'static str = $name;
const MCP_NAME_OVERRIDE: &'static str = $mcp;
const HTTP_PATH_OVERRIDE: &'static str = $path;
fn http_method_override() -> http::Method {
http::Method::$method
}
}
inventory::submit! {
$crate::OperationEntry {
name: <$op as $crate::OperationNames>::NAME_OVERRIDE,
mcp_tool_name: <$op as $crate::OperationNames>::MCP_NAME_OVERRIDE,
pack: ::core::option::Option::None,
http_method: <$op as $crate::OperationNames>::http_method_override,
http_path: <$op as $crate::OperationNames>::HTTP_PATH_OVERRIDE,
input_schema: || schemars::schema_for!($input),
output_schema: || schemars::schema_for!($output),
handler: |_entry, ctx, v| Box::pin(async move {
let input: $input =
serde_json::from_value(v).map_err(|e| $crate::OpError::BadInput(e.to_string()))?;
let out = $op::default().handle(ctx, input).await?;
serde_json::to_value(out).map_err(|e| $crate::OpError::Other(e.to_string()))
}),
}
}
};
}
pub trait OperationNames {
const NAME_OVERRIDE: &'static str;
const MCP_NAME_OVERRIDE: &'static str;
const HTTP_PATH_OVERRIDE: &'static str;
fn http_method_override() -> http::Method;
}
pub fn entries() -> Vec<&'static OperationEntry> {
static PACK_ENTRIES: std::sync::OnceLock<Vec<OperationEntry>> = std::sync::OnceLock::new();
let pack_entries = PACK_ENTRIES.get_or_init(crate::pack_verbs::registry_entries);
let mut all: Vec<&'static OperationEntry> =
inventory::iter::<OperationEntry>.into_iter().collect();
all.extend(pack_entries.iter());
all.sort_by_key(|e| e.name);
all
}