systemprompt-agent 0.63.0

Agent-to-Agent (A2A) protocol for systemprompt.io AI governance: streaming, JSON-RPC models, task lifecycle, .well-known discovery, and governed agent orchestration.
Documentation
//! JSON-RPC 2.0 error envelope construction.
//!
//! [`JsonRpcErrorBuilder`] assembles spec-coded error responses with optional
//! data payloads and structured logging; [`unauthorized_response`] and
//! [`forbidden_response`] are the auth-failure shortcuts.
//!
//! Copyright (c) systemprompt.io — Business Source License 1.1.
//! See <https://systemprompt.io> for licensing details.

use crate::models::a2a::jsonrpc::{
    JSON_RPC_VERSION_2_0, JsonRpcError, JsonRpcResponse, NumberOrString,
};
use axum::http::StatusCode;
use serde_json::{Value, json};
use systemprompt_logging::LogLevel;

pub type JsonRpcErrorResponse = JsonRpcResponse<()>;

#[derive(Debug)]
pub struct JsonRpcErrorBuilder {
    code: i32,
    message: String,
    // JSON: JSON-RPC 2.0 error `data` — the spec allows any value.
    data: Option<Value>,
    log_message: Option<String>,
    log_level: LogLevel,
}

impl JsonRpcErrorBuilder {
    pub fn new(code: i32, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
            data: None,
            log_message: None,
            log_level: LogLevel::Error,
        }
    }

    // JSON: JSON-RPC 2.0 error `data` — the spec allows any value.
    pub fn with_data(mut self, data: Value) -> Self {
        self.data = Some(data);
        self
    }

    pub fn with_log(mut self, message: impl Into<String>, level: LogLevel) -> Self {
        self.log_message = Some(message.into());
        self.log_level = level;
        self
    }

    pub fn log_error(mut self, message: impl Into<String>) -> Self {
        self.log_message = Some(message.into());
        self.log_level = LogLevel::Error;
        self
    }

    pub fn log_warn(mut self, message: impl Into<String>) -> Self {
        self.log_message = Some(message.into());
        self.log_level = LogLevel::Warn;
        self
    }

    pub fn build(self, request_id: &NumberOrString) -> JsonRpcErrorResponse {
        self.build_as(request_id)
    }

    pub fn build_as<T>(self, request_id: &NumberOrString) -> JsonRpcResponse<T> {
        if let Some(log_msg) = self.log_message {
            match self.log_level {
                LogLevel::Error => {
                    tracing::error!(topic = "a2a_jsonrpc", detail = %log_msg, "JSON-RPC error response");
                },
                LogLevel::Warn => {
                    tracing::warn!(topic = "a2a_jsonrpc", detail = %log_msg, "JSON-RPC error response");
                },
                LogLevel::Info => {
                    tracing::info!(topic = "a2a_jsonrpc", detail = %log_msg, "JSON-RPC error response");
                },
                LogLevel::Debug => {
                    tracing::debug!(topic = "a2a_jsonrpc", detail = %log_msg, "JSON-RPC error response");
                },
                LogLevel::Trace => {
                    tracing::trace!(topic = "a2a_jsonrpc", detail = %log_msg, "JSON-RPC error response");
                },
            }
        }

        JsonRpcResponse {
            jsonrpc: JSON_RPC_VERSION_2_0.to_owned(),
            result: None,
            error: Some(JsonRpcError {
                code: self.code,
                message: self.message,
                data: self.data,
            }),
            id: request_id.clone(),
        }
    }

    pub fn build_with_status(
        self,
        request_id: &NumberOrString,
    ) -> (StatusCode, JsonRpcErrorResponse) {
        let status = match self.code {
            -32600 => StatusCode::BAD_REQUEST,
            -32601 => StatusCode::NOT_FOUND,
            -32602 => StatusCode::BAD_REQUEST,
            -32603 => StatusCode::INTERNAL_SERVER_ERROR,
            -32700 => StatusCode::BAD_REQUEST,
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        };

        (status, self.build(request_id))
    }

    pub fn invalid_request() -> Self {
        Self::new(-32600, "Invalid Request")
    }

    pub fn method_not_found() -> Self {
        Self::new(-32601, "Method not found")
    }

    pub fn invalid_params() -> Self {
        Self::new(-32602, "Invalid params")
    }

    pub fn internal_error() -> Self {
        Self::new(-32603, "Internal error")
    }

    pub fn parse_error() -> Self {
        Self::new(-32700, "Parse error")
    }

    pub fn unsupported_operation() -> Self {
        Self::new(-32004, "This operation is not supported")
    }

    pub fn unauthorized(reason: impl Into<String>) -> Self {
        Self::new(-32600, "Unauthorized").with_data(json!({
            "reason": reason.into()
        }))
    }

    pub fn forbidden(reason: impl Into<String>) -> Self {
        Self::new(-32600, "Forbidden").with_data(json!({
            "reason": reason.into()
        }))
    }
}

pub fn unauthorized_response(
    reason: impl Into<String>,
    request_id: &NumberOrString,
) -> (StatusCode, JsonRpcErrorResponse) {
    let reason_str = reason.into();
    (
        StatusCode::UNAUTHORIZED,
        JsonRpcErrorBuilder::unauthorized(&reason_str)
            .log_warn(&reason_str)
            .build(request_id),
    )
}

pub fn forbidden_response(
    reason: impl Into<String>,
    request_id: &NumberOrString,
) -> (StatusCode, JsonRpcErrorResponse) {
    let reason_str = reason.into();
    (
        StatusCode::FORBIDDEN,
        JsonRpcErrorBuilder::forbidden(&reason_str)
            .log_warn(&reason_str)
            .build(request_id),
    )
}