use std::collections::{BTreeMap, HashMap};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration as StdDuration;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderValue, Method, StatusCode};
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use base64::Engine;
use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use futures::StreamExt;
use harn_vm::connectors::{JwtKeySource, JwtVerificationOptions};
use harn_vm::event_log::{AnyEventLog, EventLog, LogEvent, Topic};
use hmac::{Hmac, KeyInit, Mac};
use serde_json::{json, Value as JsonValue};
use sha2::Sha256;
use uuid::Uuid;
use crate::{
AdapterDescriptor, AuthMethodConfig, AuthPolicy, AuthRequest, AuthorizationDecision,
CallArguments, CallRequest, CallResponse, DispatchAgentEventSink, DispatchCore, DispatchError,
DispatchRuntime, ExportCatalog, HttpTlsConfig, TransportAdapter,
};
mod auth;
mod events;
mod schema;
mod tasks;
#[cfg(test)]
mod tests;
mod transport;
use schema::{derived_agent_name, load_push_configs};
pub const A2A_PROTOCOL_VERSION: &str = "0.3.0";
const A2A_VERSION_HEADER: &str = "a2a-version";
const A2A_TRACE_HEADER: &str = "a2a-trace-id";
const A2A_DEPRECATION_HEADER: &str = "deprecation";
const A2A_AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
const A2A_REST_BASE: &str = "/v1";
const REST_DEPRECATED_MESSAGE_SEND: &str = "Use A2A 0.3.0 REST path `POST /v1/message:send`.";
const REST_DEPRECATED_MESSAGE_STREAM: &str = "Use A2A 0.3.0 REST path `POST /v1/message:stream`.";
const REST_DEPRECATED_CANCEL: &str = "Use A2A 0.3.0 REST path `POST /v1/tasks/{id}:cancel`.";
const REST_DEPRECATED_RESUBSCRIBE: &str =
"Use A2A 0.3.0 REST path `POST /v1/tasks/{id}:subscribe`.";
const REST_DEPRECATED_SEND: &str =
"Use A2A 0.3.0 REST path `POST /v1/message:send` (use `configuration.blocking = true` for synchronous waits).";
const A2A_AUTH_REALM: &str = "harn-a2a";
const A2A_TASK_NOT_FOUND: i64 = -32001;
const A2A_TASK_NOT_CANCELABLE: i64 = -32002;
const A2A_UNSUPPORTED_OPERATION: i64 = -32003;
const A2A_EXTENDED_AGENT_CARD_NOT_CONFIGURED: i64 = -32007;
const A2A_PUSH_CONFIG_TOPIC: &str = "a2a.push_notification_configs";
const A2A_PUSH_CONFIG_SET_KIND: &str = "a2a.push_notification_config.set";
const A2A_PUSH_CONFIG_DELETE_KIND: &str = "a2a.push_notification_config.delete";
#[derive(Clone, Debug)]
pub struct A2aHttpServeOptions {
pub bind: SocketAddr,
pub public_url: Option<String>,
pub tls: HttpTlsConfig,
}
impl Default for A2aHttpServeOptions {
fn default() -> Self {
Self {
bind: "127.0.0.1:8080".parse().expect("valid bind addr"),
public_url: None,
tls: HttpTlsConfig::plain(),
}
}
}
pub struct A2aServerConfig {
pub core: DispatchCore,
pub agent_name: Option<String>,
pub card_signing_secret: Option<String>,
}
impl A2aServerConfig {
pub fn new(core: DispatchCore) -> Self {
Self {
agent_name: Some(derived_agent_name(core.catalog())),
core,
card_signing_secret: None,
}
}
}
pub struct A2aServer {
descriptor: AdapterDescriptor,
agent_name: String,
card_signing_secret: Option<String>,
catalog: ExportCatalog,
core: Arc<DispatchCore>,
executor: DispatchRuntime,
tasks: TaskStore,
push_configs: PushConfigStore,
}
#[derive(Clone)]
struct HttpState {
server: Arc<A2aServer>,
public_url: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum TaskStatus {
Submitted,
Working,
InputRequired,
AuthRequired,
Completed,
Failed,
Cancelled,
Rejected,
}
impl TaskStatus {
fn as_str(&self) -> &'static str {
match self {
Self::Submitted => "submitted",
Self::Working => "working",
Self::InputRequired => "input-required",
Self::AuthRequired => "auth-required",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
Self::Rejected => "rejected",
}
}
fn is_terminal(&self) -> bool {
matches!(
self,
Self::Completed | Self::Failed | Self::Cancelled | Self::Rejected
)
}
fn from_agent_terminal(kind: harn_vm::agent_events::AgentTerminalKind) -> Self {
match kind.lifecycle_state().a2a_task_state() {
Some("completed") => Self::Completed,
Some("failed") => Self::Failed,
Some("cancelled") => Self::Cancelled,
Some("paused") => Self::Working,
Some("working") => Self::Working,
Some("input-required") => Self::InputRequired,
_ => Self::Failed,
}
}
}
#[derive(Clone, Debug)]
struct TaskMessage {
id: String,
role: String,
parts: Vec<JsonValue>,
}
#[derive(Debug)]
struct TaskState {
id: String,
context_id: Option<String>,
status: TaskStatus,
history: Vec<TaskMessage>,
artifacts: Vec<JsonValue>,
metadata: BTreeMap<String, JsonValue>,
events: Vec<JsonValue>,
subscribers: Vec<UnboundedSender<JsonValue>>,
cancel_token: Option<Arc<AtomicBool>>,
}
type TaskStore = Arc<Mutex<HashMap<String, TaskState>>>;
type PushConfigStore = Arc<Mutex<HashMap<String, BTreeMap<String, JsonValue>>>>;
struct PreparedTask {
id: String,
function: String,
arguments: CallArguments,
auth: AuthRequest,
caller: String,
trace_id: Option<harn_vm::TraceId>,
actor_chain: Option<harn_vm::actor_chain::ActorChain>,
cancel_token: Arc<AtomicBool>,
}
enum RpcOutcome {
Json(JsonValue),
Sse(UnboundedReceiver<JsonValue>),
}
struct ProcessedRpc {
outcome: RpcOutcome,
deprecation: Option<&'static str>,
status: Option<StatusCode>,
auth_challenge: Option<HeaderValue>,
}
impl A2aServer {
pub fn new(config: A2aServerConfig) -> Self {
let agent_name = config
.agent_name
.unwrap_or_else(|| derived_agent_name(config.core.catalog()));
let core = Arc::new(config.core);
let catalog = core.catalog().clone();
let push_configs = Arc::new(Mutex::new(load_push_configs(&core.event_log())));
Self {
descriptor: AdapterDescriptor {
id: "a2a".to_string(),
caller_shape: "peer-agent-task".to_string(),
supports_streaming: true,
supports_cancel: true,
},
agent_name,
card_signing_secret: config.card_signing_secret,
catalog,
executor: DispatchRuntime::start("A2A", core.clone()),
core,
tasks: Arc::new(Mutex::new(HashMap::new())),
push_configs,
}
}
}
#[async_trait::async_trait(?Send)]
impl TransportAdapter for A2aServer {
fn descriptor(&self) -> AdapterDescriptor {
self.descriptor.clone()
}
}