use super::*;
#[derive(Debug, Deserialize)]
pub(super) struct PermissionReplyInput {
pub reply: String,
}
#[derive(Debug, Deserialize, Default)]
pub(super) struct QuestionReplyInput {
#[serde(default)]
pub _answers: Vec<Vec<String>>,
}
#[derive(Debug, Deserialize, Default)]
pub(super) struct QuestionAnswerInput {
pub answer: Option<String>,
}
pub(super) async fn list_permissions(State(state): State<AppState>) -> Json<Value> {
Json(json!({
"requests": state.permissions.list().await,
"rules": state.permissions.list_rules().await,
"decisions": state.permissions.list_decisions().await
}))
}
pub(super) async fn reply_permission(
State(state): State<AppState>,
Extension(tenant_context): Extension<TenantContext>,
Extension(request_principal): Extension<RequestPrincipal>,
Path(id): Path<String>,
Json(input): Json<PermissionReplyInput>,
) -> Result<Json<Value>, (StatusCode, Json<ErrorEnvelope>)> {
let accepted = matches!(
input.reply.as_str(),
"once" | "always" | "reject" | "allow" | "deny"
);
if !accepted {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorEnvelope::new(
"reply must be one of once|always|reject|allow|deny",
ErrorCode::ApprovalReplyInvalid,
)),
));
}
let outcome = state
.permissions
.reply_with_provenance(
&id,
&input.reply,
permission_actor(&request_principal),
Some("http_permission_reply".to_string()),
)
.await;
let Some(outcome) = outcome else {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorEnvelope::new(
"Permission request not found",
ErrorCode::ApprovalRequestNotFound,
)),
));
};
append_permission_decision_audit(&state, &tenant_context, &request_principal, &outcome).await;
Ok(Json(json!({
"ok": true,
"requestID": id,
"reply": input.reply,
"status": "applied",
"persistedRule": outcome.decision.standing_rule_persisted,
"standingRuleID": outcome.decision.standing_rule_id
})))
}
pub(super) async fn approve_tool_by_call(
State(state): State<AppState>,
Extension(tenant_context): Extension<TenantContext>,
Extension(request_principal): Extension<RequestPrincipal>,
Path((session_id, tool_call_id)): Path<(String, String)>,
) -> Result<Json<Value>, (StatusCode, Json<ErrorEnvelope>)> {
let outcome = state
.permissions
.reply_with_provenance(
&tool_call_id,
"allow",
permission_actor(&request_principal),
Some("tool_call_approved".to_string()),
)
.await;
let Some(outcome) = outcome else {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorEnvelope::new(
"Permission request not found",
ErrorCode::ApprovalRequestNotFound,
)),
));
};
append_permission_decision_audit(&state, &tenant_context, &request_principal, &outcome).await;
let _ = crate::audit::append_protected_audit_event(
&state,
"approval.granted",
&tandem_types::TenantContext::local_implicit(),
permission_actor(&request_principal),
json!({
"sessionID": session_id,
"toolCallID": tool_call_id,
"decision": "allow",
}),
)
.await;
Ok(Json(json!({"ok": true})))
}
pub(super) async fn deny_tool_by_call(
State(state): State<AppState>,
Extension(tenant_context): Extension<TenantContext>,
Extension(request_principal): Extension<RequestPrincipal>,
Path((session_id, tool_call_id)): Path<(String, String)>,
) -> Result<Json<Value>, (StatusCode, Json<ErrorEnvelope>)> {
let outcome = state
.permissions
.reply_with_provenance(
&tool_call_id,
"deny",
permission_actor(&request_principal),
Some("tool_call_denied".to_string()),
)
.await;
let Some(outcome) = outcome else {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorEnvelope::new(
"Permission request not found",
ErrorCode::ApprovalRequestNotFound,
)),
));
};
append_permission_decision_audit(&state, &tenant_context, &request_principal, &outcome).await;
let _ = crate::audit::append_protected_audit_event(
&state,
"approval.denied",
&tandem_types::TenantContext::local_implicit(),
permission_actor(&request_principal),
json!({
"sessionID": session_id,
"toolCallID": tool_call_id,
"decision": "deny",
}),
)
.await;
Ok(Json(json!({"ok": true})))
}
fn permission_actor(request_principal: &RequestPrincipal) -> Option<String> {
request_principal
.actor_id
.clone()
.or_else(|| Some(request_principal.source.clone()))
}
async fn append_permission_decision_audit(
state: &AppState,
tenant_context: &TenantContext,
request_principal: &RequestPrincipal,
outcome: &tandem_core::PermissionReplyOutcome,
) {
let _ = crate::audit::append_protected_audit_event(
state,
"permission.decision",
tenant_context,
permission_actor(request_principal),
json!({
"requestID": &outcome.request.id,
"sessionID": &outcome.request.session_id,
"permission": &outcome.request.permission,
"pattern": &outcome.request.pattern,
"tool": &outcome.request.tool,
"decision": &outcome.decision.decision,
"decidedAtMs": outcome.decision.decided_at_ms,
"decidedBy": &outcome.decision.decided_by,
"reason": &outcome.decision.reason,
"standingRuleID": &outcome.decision.standing_rule_id,
"standingRulePersisted": outcome.decision.standing_rule_persisted,
"principal": {
"actorID": &request_principal.actor_id,
"source": &request_principal.source,
},
"rule": outcome.rule.as_ref().map(|rule| json!({
"id": &rule.id,
"permission": &rule.permission,
"pattern": &rule.pattern,
"action": &rule.action,
"createdAtMs": &rule.created_at_ms,
"createdBy": &rule.created_by,
"sourceRequestID": &rule.source_request_id,
"provenance": &rule.provenance,
})),
}),
)
.await;
}
pub(super) async fn list_questions(State(state): State<AppState>) -> Json<Value> {
Json(json!(state.storage.list_question_requests().await))
}
pub(super) async fn reply_question(
State(state): State<AppState>,
Path(id): Path<String>,
Json(_input): Json<QuestionReplyInput>,
) -> Result<Json<Value>, StatusCode> {
let ok = state
.storage
.reply_question(&id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if ok {
state.event_bus.publish(EngineEvent::new(
"question.replied",
json!({"id": id, "ok": true}),
));
}
Ok(Json(json!({"ok": ok})))
}
pub(super) async fn reject_question(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Json<Value>, StatusCode> {
let ok = state
.storage
.reject_question(&id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if ok {
state.event_bus.publish(EngineEvent::new(
"question.replied",
json!({"id": id, "ok": false}),
));
}
Ok(Json(json!({"ok": ok})))
}
pub(super) async fn answer_question(
State(state): State<AppState>,
Path((_session_id, question_id)): Path<(String, String)>,
Json(input): Json<QuestionAnswerInput>,
) -> Result<Json<Value>, (StatusCode, Json<ErrorEnvelope>)> {
let ok = state
.storage
.reply_question(&question_id)
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorEnvelope::new(
"Failed to answer question",
ErrorCode::ApprovalPersistenceFailed,
)),
)
})?;
if !ok {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorEnvelope::new(
"Question request not found",
ErrorCode::ApprovalRequestNotFound,
)),
));
}
if ok {
state.event_bus.publish(EngineEvent::new(
"question.replied",
json!({"id": question_id, "ok": true, "answer": input.answer}),
));
}
Ok(Json(json!({"ok": true})))
}