use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::core::{
CaseId, CaseStatus, CloudEvent, Decision, Delivery, InboundEvent, PolicyDecision,
PolicyRequest, RunId, Task, TaskId, TenantId,
};
use crate::journal::RecordKind;
use crate::runtime::Runtime;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Caller {
pub actor: String,
pub roles: Vec<String>,
pub tenant: TenantId,
pub acting_as: Option<crate::core::Delegation>,
}
impl Caller {
pub fn new(actor: impl Into<String>, roles: Vec<String>) -> Self {
Self {
actor: actor.into(),
roles,
tenant: TenantId::default(),
acting_as: None,
}
}
#[must_use]
pub fn in_tenant(mut self, tenant: TenantId) -> Self {
self.tenant = tenant;
self
}
#[must_use]
pub fn acting_as(mut self, chain: crate::core::Delegation) -> Self {
self.acting_as = Some(chain);
self
}
}
pub(crate) fn peer_source(actor: &str) -> String {
format!("peer:{actor}")
}
#[async_trait::async_trait]
pub trait Authenticator: Send + Sync + std::fmt::Debug {
async fn authenticate(&self, headers: &HeaderMap) -> Result<Caller, AuthError>;
}
#[derive(Debug, thiserror::Error)]
pub enum AuthError {
#[error("no credentials were presented")]
Missing,
#[error("the credentials presented were not accepted")]
Rejected,
}
#[derive(Debug, thiserror::Error)]
pub enum ApiSetupError {
#[error(
"this runtime has no policy engine — an HTTP surface with no authorization \
layer grants every authenticated caller the whole plane; wire one (start \
with `DenyAll`) before opening a port"
)]
NoPolicy,
#[error(
"no planes were registered — this surface would authenticate callers \
and then refuse all of them; register at least one with `Planes::one`"
)]
NoPlanes,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CancelRequest {
pub reason: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DecisionRequest {
pub approved: bool,
pub reason: String,
#[serde(default)]
pub amendment: Value,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct RunView {
pub run: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub waiting_for: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exhaustion: Option<crate::core::BudgetExceeded>,
pub sealed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cancellation_requested_by: Option<String>,
pub records: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub case: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct TaskView {
pub id: String,
pub run: String,
pub kind: String,
pub justification: Value,
pub priority: String,
pub state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub case: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub due_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub assignee: Option<String>,
pub decidable_by_you: bool,
}
impl TaskView {
fn of(task: Task, caller: &Caller) -> Self {
Self {
decidable_by_you: task.may_decide(&caller.actor, &caller.roles),
assignee: task.assignee,
id: task.id.to_hex(),
run: task.run.to_string(),
kind: task.kind,
justification: serde_json::to_value(&task.justification).unwrap_or(Value::Null),
priority: task.priority.as_str().to_owned(),
state: task.state.as_str().to_owned(),
case: task.case.map(|c| c.to_string()),
due_at: task.due_at.and_then(|d| {
d.format(&time::format_description::well_known::Rfc3339)
.ok()
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Worklist {
pub tasks: Vec<TaskView>,
pub truncated: bool,
}
#[cfg(feature = "a2a-server")]
pub mod a2a;
#[cfg(feature = "a2a-server")]
mod a2a_stream;
pub mod tokens;
#[derive(Debug, Clone, Default)]
pub struct Planes {
by_tenant: std::collections::HashMap<TenantId, Arc<Runtime>>,
}
impl Planes {
#[must_use]
pub fn one(plane: Arc<Runtime>) -> Self {
Self::default().and(plane)
}
#[must_use]
pub fn and(mut self, plane: Arc<Runtime>) -> Self {
let tenant = plane.tenant().clone();
assert!(
self.by_tenant.insert(tenant.clone(), plane).is_none(),
"two planes are registered for tenant '{tenant}'. Whichever won, \
the other's skills, budgets and policy would be silently inert"
);
self
}
#[must_use]
pub fn get(&self, caller: &Caller) -> Option<&Arc<Runtime>> {
self.by_tenant.get(&caller.tenant)
}
pub async fn cross(
&self,
caller: &Caller,
target: &TenantId,
reason: &str,
) -> Result<&Arc<Runtime>, crate::core::RuntimeError> {
if caller.tenant == *target {
return Err(crate::core::RuntimeError::PlanContract(format!(
"'{target}' is the caller's own tenant, so this is not a crossing — \
use `Planes::get`. Recording it as break-glass would bury the real \
crossings among routine reads"
)));
}
let plane = self
.by_tenant
.get(target)
.ok_or_else(|| crate::core::RuntimeError::UnknownTenant(target.to_string()))?;
plane
.record_break_glass(&caller.actor, &caller.roles, reason)
.await?;
Ok(plane)
}
pub fn tenants(&self) -> impl Iterator<Item = &TenantId> {
self.by_tenant.keys()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_tenant.is_empty()
}
}
impl From<Arc<Runtime>> for Planes {
fn from(plane: Arc<Runtime>) -> Self {
Self::one(plane)
}
}
struct Session {
caller: Caller,
plane: Arc<Runtime>,
}
#[derive(Clone)]
pub struct Api {
planes: Planes,
auth: Arc<dyn Authenticator>,
limit: usize,
history: usize,
}
impl std::fmt::Debug for Api {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Api")
.field("tenants", &self.planes.tenants().collect::<Vec<_>>())
.field("limit", &self.limit)
.field("history", &self.history)
.finish_non_exhaustive()
}
}
impl Api {
pub const DEFAULT_LIMIT: usize = 100;
pub const DEFAULT_HISTORY_LIMIT: usize = 200;
pub fn new(
planes: impl Into<Planes>,
auth: Arc<dyn Authenticator>,
) -> Result<Self, ApiSetupError> {
let planes = planes.into();
if planes.is_empty() {
return Err(ApiSetupError::NoPlanes);
}
for plane in planes.by_tenant.values() {
if plane.policy().is_none() {
return Err(ApiSetupError::NoPolicy);
}
}
Ok(Self {
planes,
auth,
limit: Self::DEFAULT_LIMIT,
history: Self::DEFAULT_HISTORY_LIMIT,
})
}
#[must_use]
pub const fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
#[must_use]
pub const fn history_limit(mut self, limit: usize) -> Self {
self.history = limit;
self
}
pub fn router(self) -> Router {
Router::new()
.route("/runs", get(runs_by_outcome))
.route("/runs/{run}", get(run_view))
.route("/runs/{run}/cancel", post(cancel_run))
.route("/tasks", get(worklist))
.route("/tasks/{task}", get(task_view))
.route("/tasks/{task}/claim", post(claim))
.route("/tasks/{task}/release", post(release))
.route("/tasks/{task}/takeover", post(take_over))
.route("/tasks/{task}/decide", post(decide))
.route("/cases", get(cases_by_status))
.route("/obligations", get(breached_obligations))
.route("/cases/{case}", get(case_view))
.route("/events", post(deliver))
.with_state(self)
}
async fn gate(
&self,
headers: &HeaderMap,
action: &str,
resource: &str,
) -> Result<Session, ApiError> {
let caller = self.auth.authenticate(headers).await?;
let plane = self.planes.get(&caller).ok_or_else(|| {
ApiError(
StatusCode::FORBIDDEN,
"this deployment serves no plane for your tenant".to_owned(),
)
})?;
let policy = plane.policy().ok_or_else(|| {
ApiError(StatusCode::FORBIDDEN, "this plane is ungoverned".to_owned())
})?;
let context = json!({ "roles": caller.roles, "tenant": caller.tenant.as_str() });
let decision = policy.authorize(&PolicyRequest {
principal: &caller.actor,
action,
resource,
context: &context,
});
match decision {
PolicyDecision::Permit => Ok(Session {
caller,
plane: Arc::clone(plane),
}),
PolicyDecision::Deny { reason } => Err(ApiError(StatusCode::FORBIDDEN, reason)),
PolicyDecision::Malformed { reason } => {
tracing::error!(target: "agentplane::api", policy_error = true, reason, "the policy set could not be evaluated");
Err(ApiError(StatusCode::INTERNAL_SERVER_ERROR, reason))
}
}
}
}
pub mod action {
pub const RUN_READ: &str = "api:run.read";
pub const RUN_LIST: &str = "api:run.list";
pub const RUN_CANCEL: &str = "api:run.cancel";
pub const TASK_LIST: &str = "api:task.list";
pub const TASK_READ: &str = "api:task.read";
pub const TASK_CLAIM: &str = "api:task.claim";
pub const TASK_RELEASE: &str = "api:task.release";
pub const TASK_TAKEOVER: &str = "api:task.takeover";
pub const TASK_DECIDE: &str = "api:task.decide";
pub const CASE_READ: &str = "api:case.read";
pub const CASE_LIST: &str = "api:case.list";
pub const OBLIGATION_LIST: &str = "api:obligation.list";
pub const EVENT_DELIVER: &str = "api:event.deliver";
pub const ALL: &[&str] = &[
RUN_READ,
RUN_LIST,
RUN_CANCEL,
TASK_TAKEOVER,
TASK_LIST,
TASK_READ,
TASK_CLAIM,
TASK_RELEASE,
TASK_DECIDE,
CASE_READ,
CASE_LIST,
OBLIGATION_LIST,
EVENT_DELIVER,
];
}
struct ApiError(StatusCode, String);
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(self.0, Json(json!({ "error": self.1 }))).into_response()
}
}
impl From<AuthError> for ApiError {
fn from(e: AuthError) -> Self {
Self(StatusCode::UNAUTHORIZED, e.to_string())
}
}
fn bad(what: &str) -> ApiError {
ApiError(StatusCode::BAD_REQUEST, format!("not a {what} id"))
}
fn unavailable(what: &str) -> ApiError {
ApiError(
StatusCode::NOT_IMPLEMENTED,
format!("this plane has no {what} store"),
)
}
fn store_failed() -> ApiError {
ApiError(
StatusCode::INTERNAL_SERVER_ERROR,
"the store is unavailable".into(),
)
}
fn not_found(what: &str) -> ApiError {
ApiError(StatusCode::NOT_FOUND, format!("no such {what}"))
}
async fn run_view(
State(api): State<Api>,
headers: HeaderMap,
Path(run): Path<String>,
) -> Result<Json<RunView>, ApiError> {
let s = api.gate(&headers, action::RUN_READ, &run).await?;
let id = RunId::parse(&run).map_err(|_| bad("run"))?;
let records = s
.plane
.journal()
.read(id, 1)
.await
.map_err(|_| store_failed())?;
let Some(last) = records.last() else {
return Err(not_found("run"));
};
let (status, waiting_for, reason, exhaustion) = match last.kind() {
RecordKind::RunSuspended { reason } => {
("suspended".to_owned(), Some(reason.to_string()), None, None)
}
RecordKind::RunConcluded {
outcome,
reason,
exhaustion,
..
} => (outcome.clone(), None, reason.clone(), exhaustion.clone()),
_ => ("running".to_owned(), None, None, None),
};
let sealed = s
.plane
.journal()
.inclusion_proof(id)
.await
.map_err(|_| store_failed())?
.is_some();
let case = records
.iter()
.find_map(|r| r.body.case.map(|c| c.to_string()));
let cancellation_requested_by = s
.plane
.cancellation(id)
.await
.map_err(|_| store_failed())?
.map(|c| c.actor);
Ok(Json(RunView {
run: id.to_string(),
status,
waiting_for,
reason,
exhaustion,
sealed,
cancellation_requested_by,
records: records.len() as u64,
case,
}))
}
async fn cancel_run(
State(api): State<Api>,
headers: HeaderMap,
Path(run): Path<String>,
Json(body): Json<CancelRequest>,
) -> Result<(StatusCode, Json<Value>), ApiError> {
let s = api.gate(&headers, action::RUN_CANCEL, &run).await?;
let id = RunId::parse(&run).map_err(|_| bad("run"))?;
let fresh = s
.plane
.request_cancel(id, &s.caller.actor, &body.reason)
.await
.map_err(|e| match e {
crate::core::RuntimeError::Store(crate::core::StoreError::NotFound(_)) => {
not_found("run")
}
crate::core::RuntimeError::Store(_) => store_failed(),
other => ApiError(StatusCode::CONFLICT, other.to_string()),
})?;
Ok((
StatusCode::ACCEPTED,
Json(json!({
"requested_by": s.caller.actor,
"recorded": fresh,
})),
))
}
async fn runs_by_outcome(
State(api): State<Api>,
headers: HeaderMap,
Query(q): Query<OutcomeQuery>,
) -> Result<Json<Value>, ApiError> {
let outcome = q.outcome.unwrap_or_else(|| "quarantined".to_owned());
let s = api.gate(&headers, action::RUN_LIST, &outcome).await?;
let mut found = s
.plane
.journal()
.runs_by_outcome(&outcome, api.limit + 1)
.await
.map_err(|_| store_failed())?;
let truncated = found.len() > api.limit;
found.truncate(api.limit);
Ok(Json(json!({
"outcome": outcome,
"runs": found.iter().map(ToString::to_string).collect::<Vec<_>>(),
"truncated": truncated,
})))
}
#[derive(serde::Deserialize)]
struct OutcomeQuery {
outcome: Option<String>,
}
async fn worklist(State(api): State<Api>, headers: HeaderMap) -> Result<Json<Worklist>, ApiError> {
let s = api.gate(&headers, action::TASK_LIST, "*").await?;
let tasks = s.plane.tasks().ok_or_else(|| unavailable("task"))?;
let mut queued = tasks
.queue(&s.caller.roles, api.limit + 1)
.await
.map_err(|_| store_failed())?;
let truncated = queued.len() > api.limit;
queued.truncate(api.limit);
Ok(Json(Worklist {
tasks: queued
.into_iter()
.map(|t| TaskView::of(t, &s.caller))
.collect(),
truncated,
}))
}
async fn task_view(
State(api): State<Api>,
headers: HeaderMap,
Path(task): Path<String>,
) -> Result<Json<TaskView>, ApiError> {
let s = api.gate(&headers, action::TASK_READ, &task).await?;
let id = TaskId::parse(&task).map_err(|_| bad("task"))?;
let tasks = s.plane.tasks().ok_or_else(|| unavailable("task"))?;
let found = tasks
.task(id)
.await
.map_err(|_| store_failed())?
.ok_or_else(|| not_found("task"))?;
Ok(Json(TaskView::of(found, &s.caller)))
}
async fn claim(
State(api): State<Api>,
headers: HeaderMap,
Path(task): Path<String>,
) -> Result<Json<TaskView>, ApiError> {
let s = api.gate(&headers, action::TASK_CLAIM, &task).await?;
let id = TaskId::parse(&task).map_err(|_| bad("task"))?;
let tasks = s.plane.tasks().ok_or_else(|| unavailable("task"))?;
let claimed = tasks
.claim(id, &s.caller.actor, &s.caller.roles)
.await
.map_err(|e| claim_refused(&e))?;
Ok(Json(TaskView::of(claimed, &s.caller)))
}
async fn release(
State(api): State<Api>,
headers: HeaderMap,
Path(task): Path<String>,
) -> Result<StatusCode, ApiError> {
let s = api.gate(&headers, action::TASK_RELEASE, &task).await?;
let id = TaskId::parse(&task).map_err(|_| bad("task"))?;
let tasks = s.plane.tasks().ok_or_else(|| unavailable("task"))?;
tasks
.release(id, &s.caller.actor)
.await
.map_err(|e| claim_refused(&e))?;
Ok(StatusCode::NO_CONTENT)
}
async fn take_over(
State(api): State<Api>,
headers: HeaderMap,
Path(task): Path<String>,
Json(body): Json<TakeOverBody>,
) -> Result<Json<TaskView>, ApiError> {
let s = api.gate(&headers, action::TASK_TAKEOVER, &task).await?;
let id = TaskId::parse(&task).map_err(|_| bad("task"))?;
let tasks = s.plane.tasks().ok_or_else(|| unavailable("task"))?;
let taken = tasks
.take_over(id, &body.from, &s.caller.actor, &s.caller.roles)
.await
.map_err(|e| claim_refused(&e))?;
Ok(Json(TaskView::of(taken, &s.caller)))
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct TakeOverBody {
from: String,
}
fn claim_refused(e: &crate::case::ClaimError) -> ApiError {
use crate::case::ClaimError;
let status = match *e {
ClaimError::Excluded { .. } | ClaimError::WrongRole { .. } => StatusCode::FORBIDDEN,
ClaimError::NotFound(_) => StatusCode::NOT_FOUND,
ClaimError::AlreadyClaimed { .. }
| ClaimError::NotPending { .. }
| ClaimError::NotHeld { .. } => StatusCode::CONFLICT,
ClaimError::Store(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let detail = if status == StatusCode::INTERNAL_SERVER_ERROR {
"the store is unavailable".to_owned()
} else {
e.to_string()
};
ApiError(status, detail)
}
async fn decide(
State(api): State<Api>,
headers: HeaderMap,
Path(task): Path<String>,
Json(body): Json<DecisionRequest>,
) -> Result<Json<Value>, ApiError> {
let s = api.gate(&headers, action::TASK_DECIDE, &task).await?;
let id = TaskId::parse(&task).map_err(|_| bad("task"))?;
let decision = Decision {
approved: body.approved,
actor: s.caller.actor.clone(),
reason: body.reason,
amendment: body.amendment,
};
s.plane
.decide_task(id, &decision, &s.caller.roles)
.await
.map_err(|e| match e {
crate::core::RuntimeError::TaskClaim(claim) => claim_refused(&claim),
crate::core::RuntimeError::PolicyDenied(_) => {
ApiError(StatusCode::FORBIDDEN, e.to_string())
}
crate::core::RuntimeError::Store(_) => store_failed(),
other => ApiError(StatusCode::CONFLICT, other.to_string()),
})?;
Ok(Json(json!({
"decided_by": s.caller.actor,
"approved": decision.approved,
})))
}
async fn cases_by_status(
State(api): State<Api>,
headers: HeaderMap,
Query(q): Query<StatusQuery>,
) -> Result<Json<Value>, ApiError> {
let asked = q.status.unwrap_or_else(|| "escalated".to_owned());
let s = api.gate(&headers, action::CASE_LIST, &asked).await?;
let status = CaseStatus::parse(&asked).ok_or_else(|| bad("status"))?;
let cases = s.plane.cases().ok_or_else(|| unavailable("case"))?;
let mut found = cases
.by_status(status, api.limit + 1)
.await
.map_err(|_| store_failed())?;
let truncated = found.len() > api.limit;
found.truncate(api.limit);
Ok(Json(json!({
"status": status.as_str(),
"cases": found
.iter()
.map(|c| serde_json::to_value(c).unwrap_or(Value::Null))
.collect::<Vec<_>>(),
"truncated": truncated,
})))
}
#[derive(serde::Deserialize)]
struct StatusQuery {
status: Option<String>,
}
async fn breached_obligations(
State(api): State<Api>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let s = api
.gate(&headers, action::OBLIGATION_LIST, "breached")
.await?;
let cases = s.plane.cases().ok_or_else(|| unavailable("case"))?;
let mut found = cases
.breached(api.limit + 1)
.await
.map_err(|_| store_failed())?;
let truncated = found.len() > api.limit;
found.truncate(api.limit);
Ok(Json(json!({
"obligations": found
.iter()
.map(|d| serde_json::to_value(d).unwrap_or(Value::Null))
.collect::<Vec<_>>(),
"truncated": truncated,
})))
}
async fn case_view(
State(api): State<Api>,
headers: HeaderMap,
Path(case): Path<String>,
) -> Result<Json<Value>, ApiError> {
let s = api.gate(&headers, action::CASE_READ, &case).await?;
let id = CaseId::parse(&case).map_err(|_| bad("case"))?;
let cases = s.plane.cases().ok_or_else(|| unavailable("case"))?;
let found = cases
.case(id)
.await
.map_err(|_| store_failed())?
.ok_or_else(|| not_found("case"))?;
let deadlines = cases.deadlines(id).await.map_err(|_| store_failed())?;
let mut history = s
.plane
.journal()
.case_history(id, api.history + 1)
.await
.map_err(|_| store_failed())?;
let history_truncated = history.len() > api.history;
history.truncate(api.history);
Ok(Json(json!({
"case": serde_json::to_value(&found).unwrap_or(Value::Null),
"deadlines": serde_json::to_value(&deadlines).unwrap_or(Value::Null),
"history": history
.iter()
.map(|r| json!({
"seq": r.seq(),
"run": r.body.run.to_string(),
"kind": r.kind().kind_str(),
"record": serde_json::to_value(r.kind()).unwrap_or(Value::Null),
}))
.collect::<Vec<_>>(),
"history_truncated": history_truncated,
})))
}
#[derive(serde::Deserialize)]
struct DeliverBody {
id: String,
kind: String,
#[serde(default)]
correlation: Vec<crate::core::CorrelationKey>,
payload: Value,
}
enum DeliverInput {
Native(DeliverBody),
Cloud(CloudEvent),
}
impl DeliverInput {
fn kind(&self) -> &str {
match self {
Self::Native(body) => &body.kind,
Self::Cloud(event) => event.event_type(),
}
}
fn into_event(self, source: String) -> InboundEvent {
match self {
Self::Native(body) => {
let mut event = InboundEvent::new(source, body.id, body.kind, body.payload);
event.correlation = body.correlation;
event
}
Self::Cloud(event) => event.into_inbound(source),
}
}
}
fn parse_delivery(headers: &HeaderMap, body: &[u8]) -> Result<DeliverInput, ApiError> {
let structured = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(crate::core::is_cloudevent_media_type);
let binary = headers.keys().any(|name| {
name.as_str()
.starts_with(crate::core::CLOUDEVENT_HEADER_PREFIX)
});
if structured || binary {
let mut pairs: Vec<(&str, &str)> = Vec::with_capacity(headers.len());
for (name, value) in headers {
match value.to_str() {
Ok(value) => pairs.push((name.as_str(), value)),
Err(_)
if name
.as_str()
.starts_with(crate::core::CLOUDEVENT_HEADER_PREFIX) =>
{
return Err(ApiError(
StatusCode::BAD_REQUEST,
format!(
"header '{name}' is not visible ASCII — the CloudEvents HTTP \
binding percent-encodes non-ASCII attribute values"
),
));
}
Err(_) => {}
}
}
return CloudEvent::from_http(pairs, body)
.map(DeliverInput::Cloud)
.map_err(|error| ApiError(StatusCode::BAD_REQUEST, error.to_string()));
}
let body = serde_json::from_slice::<DeliverBody>(body)
.map_err(|error| ApiError(StatusCode::BAD_REQUEST, error.to_string()))?;
if body.id.is_empty() {
return Err(ApiError(
StatusCode::BAD_REQUEST,
"an event needs a non-empty 'id'".to_owned(),
));
}
if body.kind.is_empty() {
return Err(ApiError(
StatusCode::BAD_REQUEST,
"an event needs a non-empty 'kind'".to_owned(),
));
}
Ok(DeliverInput::Native(body))
}
async fn deliver(
State(api): State<Api>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Json<Value>, ApiError> {
let input = parse_delivery(&headers, &body)?;
let s = api
.gate(&headers, action::EVENT_DELIVER, input.kind())
.await?;
let event = input.into_event(peer_source(&s.caller.actor));
let delivery = s.plane.deliver(&event).await.map_err(|e| match e {
crate::core::RuntimeError::Store(_) => {
ApiError(StatusCode::SERVICE_UNAVAILABLE, e.to_string())
}
_ => ApiError(StatusCode::CONFLICT, e.to_string()),
})?;
Ok(Json(match delivery {
Delivery::Resumed { run } => json!({ "delivery": "resumed", "run": run.to_string() }),
Delivery::Buffered => json!({ "delivery": "buffered" }),
Delivery::Duplicate => json!({ "delivery": "duplicate" }),
}))
}