use super::*;
pub(crate) async fn list_pending_approvals_handler(
State(state): State<Arc<ProxyState>>,
Query(query): Query<PendingQuery>,
) -> Response {
match handle_list_pending(&state.approval_admin, query) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn get_approval_handler(
State(state): State<Arc<ProxyState>>,
Path(approval_id): Path<String>,
) -> Response {
match handle_get_approval(&state.approval_admin, &approval_id) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn respond_approval_handler(
State(state): State<Arc<ProxyState>>,
Path(approval_id): Path<String>,
body: Result<Json<RespondRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid approval response payload: {error}"
)));
}
};
let now = chrono::Utc::now().timestamp() as u64;
match handle_respond(&state.approval_admin, &approval_id, body, now) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn batch_respond_approvals_handler(
State(state): State<Arc<ProxyState>>,
body: Result<Json<BatchRespondRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid batch approval payload: {error}"
)));
}
};
let now = chrono::Utc::now().timestamp() as u64;
match handle_batch_respond(&state.approval_admin, body, now) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn create_threshold_proposal_handler(
State(state): State<Arc<ProxyState>>,
body: Result<Json<CreateThresholdProposalRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid threshold approval proposal payload: {error}"
)));
}
};
let now = chrono::Utc::now().timestamp() as u64;
match handle_create_threshold_proposal(&state.approval_admin, body, now) {
Ok(response) => approval_json(StatusCode::CREATED, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn get_threshold_proposal_handler(
State(state): State<Arc<ProxyState>>,
Path(proposal_id): Path<String>,
) -> Response {
match handle_get_threshold_proposal(&state.approval_admin, &proposal_id) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn submit_threshold_approval_handler(
State(state): State<Arc<ProxyState>>,
Path(proposal_id): Path<String>,
body: Result<Json<SubmitThresholdApprovalRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid threshold approval token payload: {error}"
)));
}
};
let now = chrono::Utc::now().timestamp() as u64;
match handle_submit_threshold_approval(&state.approval_admin, &proposal_id, body, now) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
pub(crate) async fn deliver_threshold_approval_handler(
State(state): State<Arc<ProxyState>>,
Path(proposal_id): Path<String>,
) -> Response {
let now = chrono::Utc::now().timestamp() as u64;
match handle_deliver_threshold_approval(&state.approval_admin, &proposal_id, now) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SubmitApprovalRequest {
capability_id: String,
tool_server: String,
tool_name: String,
parameter_hash: String,
requested_by: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
policy_id: Option<String>,
#[serde(default)]
ttl_seconds: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
action: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
triggered_by: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SubmitApprovalResponse {
approval_id: String,
expires_at: u64,
created_at: u64,
trusted_approvers: Vec<String>,
}
pub(crate) async fn submit_approval_handler(
State(state): State<Arc<ProxyState>>,
body: Result<Json<SubmitApprovalRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid approval submit payload: {error}"
)));
}
};
let now = chrono::Utc::now().timestamp() as u64;
let ttl = if body.ttl_seconds == 0 {
3600
} else {
body.ttl_seconds.min(3600)
};
let approval_id = format!("ap-{}", uuid::Uuid::now_v7());
let approver_pubkey = state.signer_keypair.public_key();
let parsed_subject = PublicKey::from_hex(&body.requested_by).ok();
let stored_subject_id = if parsed_subject.is_some() {
body.requested_by.clone()
} else {
approver_pubkey.to_hex()
};
let stored_subject_pubkey = parsed_subject
.clone()
.or_else(|| Some(approver_pubkey.clone()));
let approval = ApprovalRequest {
approval_id: approval_id.clone(),
policy_id: body
.policy_id
.unwrap_or_else(|| "policy-hermes-hitl".to_string()),
subject_id: stored_subject_id,
capability_id: body.capability_id.clone(),
subject_public_key: stored_subject_pubkey,
tool_server: body.tool_server.clone(),
tool_name: body.tool_name.clone(),
action: body.action.unwrap_or_else(|| "invoke".to_string()),
parameter_hash: body.parameter_hash.clone(),
expires_at: now.saturating_add(ttl),
callback_hint: None,
created_at: now,
summary: body
.summary
.unwrap_or_else(|| format!("{}/{}", body.tool_server, body.tool_name)),
governed_intent: None,
trusted_approvers: vec![approver_pubkey.clone()],
triggered_by: body.triggered_by,
};
if let Err(error) = state.approval_admin.store().store_pending(&approval) {
let handler_err: ApprovalHandlerError = error.into();
return approval_error_response(handler_err);
}
let response = SubmitApprovalResponse {
approval_id,
expires_at: approval.expires_at,
created_at: approval.created_at,
trusted_approvers: vec![approver_pubkey.to_hex()],
};
approval_json(StatusCode::CREATED, response)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct OperatorRespondRequest {
outcome: ApprovalOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
}
pub(crate) async fn operator_respond_approval_handler(
State(state): State<Arc<ProxyState>>,
Path(approval_id): Path<String>,
body: Result<Json<OperatorRespondRequest>, axum::extract::rejection::JsonRejection>,
) -> Response {
let Json(body) = match body {
Ok(body) => body,
Err(error) => {
return approval_error_response(ApprovalHandlerError::BadRequest(format!(
"invalid operator approval payload: {error}"
)));
}
};
let pending = match state.approval_admin.store().get_pending(&approval_id) {
Ok(Some(request)) => request,
Ok(None) => {
return approval_error_response(ApprovalHandlerError::NotFound(approval_id));
}
Err(error) => {
return approval_error_response(error.into());
}
};
let approver_pubkey = state.signer_keypair.public_key();
if !pending.trusted_approvers.contains(&approver_pubkey) {
return approval_error_response(ApprovalHandlerError::Rejected(
"sidecar signer is not a trusted approver for this request".into(),
));
}
let subject_pubkey = pending
.subject_public_key
.clone()
.or_else(|| PublicKey::from_hex(&pending.subject_id).ok())
.unwrap_or_else(|| approver_pubkey.clone());
let now = chrono::Utc::now().timestamp() as u64;
let decision = match body.outcome {
ApprovalOutcome::Approved => GovernedApprovalDecision::Approved,
ApprovalOutcome::Denied => GovernedApprovalDecision::Denied,
};
let token_body = GovernedApprovalTokenBody {
id: format!("op-tok-{}", uuid::Uuid::now_v7()),
approver: approver_pubkey.clone(),
subject: subject_pubkey,
governed_intent_hash: pending.parameter_hash.clone(),
request_id: approval_id.clone(),
threshold_proposal_hash: None,
issued_at: now,
expires_at: now.saturating_add(600),
decision,
};
let token = match GovernedApprovalToken::sign(token_body, &state.signer_keypair) {
Ok(token) => token,
Err(error) => {
return internal_json_error_response(
"operator_respond_sign_failed",
&format!("failed to sign operator approval token: {error}"),
);
}
};
let respond_body = RespondRequest {
outcome: body.outcome.clone(),
reason: body.reason,
approver: approver_pubkey,
token,
};
match handle_respond(&state.approval_admin, &approval_id, respond_body, now) {
Ok(response) => approval_json(StatusCode::OK, response),
Err(error) => approval_error_response(error),
}
}