use std::{
collections::HashMap,
io,
sync::{Arc, Mutex, OnceLock},
time::Duration,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::{
io::AsyncWriteExt,
net::unix::OwnedWriteHalf,
sync::{
Notify,
mpsc::{self, UnboundedSender, error::SendError},
},
task::JoinHandle,
time::timeout,
};
use uuid::Uuid;
use crate::configuration::SessionType;
use crate::runtime::inscriptions::emit_inscription;
use super::identity::{PrincipalType, classify_principal_id, scope_permits, split_principal_id};
use super::{RelayRequest, RelayResponse};
const RELAY_CONNECTION_WRITE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub(super) struct HelloFrame {
pub(super) schema_version: String,
pub(super) principal_id: String,
pub(super) identity_token: String,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub(super) enum RegistryKey {
Session {
bundle_name: String,
session_id: String,
},
RelayWide {
principal_id: String,
},
}
#[derive(Clone, Debug)]
pub(super) struct StreamRegistration {
pub(super) key: RegistryKey,
pub(super) stream_id: String,
}
impl StreamRegistration {
pub(super) fn requester_session_id(&self) -> &str {
match &self.key {
RegistryKey::Session { session_id, .. } => session_id.as_str(),
RegistryKey::RelayWide { principal_id } => principal_id.as_str(),
}
}
pub(super) fn bundle_name(&self) -> Option<&str> {
match &self.key {
RegistryKey::Session { bundle_name, .. } => Some(bundle_name.as_str()),
RegistryKey::RelayWide { .. } => None,
}
}
}
pub(super) type SharedStreamWriter = UnboundedSender<Vec<u8>>;
pub(super) type StreamRevokeSignal = Arc<Notify>;
#[derive(Clone, Debug, PartialEq)]
pub(super) enum IncomingFrame {
Hello(HelloFrame),
Request {
request_id: Option<String>,
namespace: Option<String>,
request: RelayRequest,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "frame", rename_all = "snake_case")]
enum IncomingEnvelope {
Hello {
schema_version: String,
principal_id: String,
identity_token: String,
},
Request {
#[serde(default)]
request_id: Option<String>,
#[serde(default)]
namespace: Option<String>,
request: RelayRequest,
},
}
#[derive(Clone, Debug, Serialize)]
#[serde(tag = "frame", rename_all = "snake_case")]
pub(super) enum OutgoingFrame<'a> {
HelloAck {
schema_version: &'a str,
principal_id: &'a str,
},
Response {
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<&'a str>,
response: &'a RelayResponse,
},
Event {
event: &'a RelayStreamEvent,
},
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub(super) struct RelayStreamEvent {
pub(super) event_type: String,
pub(super) bundle_name: String,
pub(super) target_session: String,
pub(super) created_at: String,
pub(super) payload: Value,
}
#[derive(Clone, Debug)]
struct RegistryEntry {
stream_id: Option<String>,
session_type: SessionType,
writer: Option<SharedStreamWriter>,
authenticated_identity: Option<String>,
revoke: Option<StreamRevokeSignal>,
scope: Option<String>,
}
#[derive(Default)]
struct StreamRegistry {
entries: Mutex<HashMap<RegistryKey, RegistryEntry>>,
}
static STREAM_REGISTRY: OnceLock<StreamRegistry> = OnceLock::new();
fn registry_key_for_target(bundle_name: &str, session_id: &str) -> RegistryKey {
match classify_principal_id(session_id) {
Some(PrincipalType::User | PrincipalType::Application | PrincipalType::Relay) => {
RegistryKey::RelayWide {
principal_id: session_id.to_string(),
}
}
_ => RegistryKey::Session {
bundle_name: bundle_name.to_string(),
session_id: session_id.to_string(),
},
}
}
pub(super) fn parse_incoming_frame(line: &str) -> Result<IncomingFrame, io::Error> {
let envelope = serde_json::from_str::<IncomingEnvelope>(line).map_err(io::Error::other)?;
match envelope {
IncomingEnvelope::Hello {
schema_version,
principal_id,
identity_token,
} => Ok(IncomingFrame::Hello(HelloFrame {
schema_version,
principal_id,
identity_token,
})),
IncomingEnvelope::Request {
request_id,
namespace,
request,
} => Ok(IncomingFrame::Request {
request_id,
namespace,
request,
}),
}
}
pub(super) fn encode_outgoing_frame(frame: OutgoingFrame<'_>) -> Result<String, io::Error> {
serde_json::to_string(&frame).map_err(io::Error::other)
}
pub(super) fn spawn_stream_writer(
mut write_half: OwnedWriteHalf,
) -> (SharedStreamWriter, JoinHandle<()>) {
let (sender, mut receiver) = mpsc::unbounded_channel::<Vec<u8>>();
let handle = tokio::spawn(async move {
while let Some(bytes) = receiver.recv().await {
let write = async {
write_half.write_all(&bytes).await?;
write_half.flush().await
};
match timeout(relay_connection_write_timeout(), write).await {
Ok(Ok(())) => {}
Ok(Err(source)) => {
note_write_timeout(&source);
break;
}
Err(_elapsed) => {
let timeout_error =
io::Error::new(io::ErrorKind::TimedOut, "relay connection write timed out");
note_write_timeout(&timeout_error);
break;
}
}
}
});
(sender, handle)
}
fn relay_connection_write_timeout() -> Duration {
std::env::var("AGENTMUX_RELAY_CONNECTION_WRITE_TIMEOUT_MS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value > 0)
.map(Duration::from_millis)
.unwrap_or(RELAY_CONNECTION_WRITE_TIMEOUT)
}
pub(super) fn register_stream(
key: RegistryKey,
session_type: SessionType,
writer: SharedStreamWriter,
authenticated_identity: Option<String>,
revoke: StreamRevokeSignal,
scope: Option<String>,
) -> Result<RegisterStreamOutcome, io::Error> {
let registry = stream_registry();
let mut entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
if let Some(entry) = entries.get(&key)
&& entry.stream_id.is_some()
&& let Some(existing_writer) = entry.writer.as_ref()
&& !existing_writer.is_closed()
{
return Ok(RegisterStreamOutcome::IdentityClaimConflict {
existing_connection_id: entry.stream_id.clone(),
});
}
let stream_id = Uuid::new_v4().to_string();
entries.insert(
key.clone(),
RegistryEntry {
stream_id: Some(stream_id.clone()),
session_type,
writer: Some(writer),
authenticated_identity,
revoke: Some(revoke),
scope,
},
);
Ok(RegisterStreamOutcome::Registered(StreamRegistration {
key,
stream_id,
}))
}
#[derive(Clone, Debug)]
pub(super) enum RegisterStreamOutcome {
Registered(StreamRegistration),
IdentityClaimConflict {
existing_connection_id: Option<String>,
},
}
pub(super) fn registration_is_current(
registration: &StreamRegistration,
) -> Result<bool, io::Error> {
let registry = stream_registry();
let entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
Ok(entries
.get(®istration.key)
.is_some_and(|entry| entry.stream_id.as_deref() == Some(registration.stream_id.as_str())))
}
pub(super) fn unregister_stream(registration: &StreamRegistration) -> Result<(), io::Error> {
let registry = stream_registry();
let mut entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
if let Some(entry) = entries.get_mut(®istration.key)
&& entry
.stream_id
.as_deref()
.is_some_and(|stream_id| stream_id == registration.stream_id.as_str())
{
entry.stream_id = None;
entry.writer = None;
entry.revoke = None;
}
Ok(())
}
fn evict_streams<F>(selector: F, response: &RelayResponse) -> usize
where
F: Fn(&RegistryKey, &RegistryEntry) -> bool,
{
let registry = stream_registry();
let targets = {
let Ok(mut entries) = registry.entries.lock() else {
return 0;
};
let mut targets = Vec::new();
for (key, entry) in entries.iter_mut() {
if !selector(key, entry) {
continue;
}
let (Some(writer), Some(revoke)) = (entry.writer.clone(), entry.revoke.clone()) else {
continue;
};
entry.stream_id = None;
entry.writer = None;
entry.revoke = None;
targets.push((writer, revoke));
}
targets
};
let count = targets.len();
for (writer, revoke) in targets {
let _ = write_stream_frame_to_writer(
&writer,
OutgoingFrame::Response {
request_id: None,
response,
},
);
revoke.notify_one();
}
count
}
pub(super) fn revoke_streams_for_identity(principal_id: &str, response: &RelayResponse) -> usize {
evict_streams(
|_key, entry| entry.authenticated_identity.as_deref() == Some(principal_id),
response,
)
}
pub(super) fn evict_streams_for_bundle(bundle_name: &str, response: &RelayResponse) -> usize {
evict_streams(
|key, _entry| {
matches!(
key,
RegistryKey::Session { bundle_name: bound, .. } if bound == bundle_name
)
},
response,
)
}
pub(super) fn notify_trusted_hosts_of_revocation(
revoked_principal_id: &str,
template: &RelayStreamEvent,
) -> usize {
let registry = stream_registry();
let targets = {
let Ok(entries) = registry.entries.lock() else {
return 0;
};
let mut targets = Vec::new();
for (key, entry) in entries.iter() {
let Some(writer) = entry.writer.clone() else {
continue;
};
if !scope_permits(entry.scope.as_deref(), revoked_principal_id) {
continue;
}
let RegistryKey::RelayWide { principal_id } = key else {
continue;
};
targets.push((writer, principal_id.clone()));
}
targets
};
let count = targets.len();
for (writer, host_principal_id) in targets {
let mut event = template.clone();
event.target_session = host_principal_id.clone();
if let Some((_, namespace)) = split_principal_id(host_principal_id.as_str()) {
event.bundle_name = namespace.to_string();
}
let _ = write_stream_frame_to_writer(&writer, OutgoingFrame::Event { event: &event });
}
count
}
pub(super) fn resolve_registered_session_type(
bundle_name: &str,
session_id: &str,
) -> Result<Option<SessionType>, io::Error> {
let registry = stream_registry();
let entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
let key = registry_key_for_target(bundle_name, session_id);
Ok(entries.get(&key).map(|entry| entry.session_type))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum StreamEventSendOutcome {
Delivered,
NoUiEndpoint,
Disconnected,
}
pub(super) fn list_registered_ui_sessions_for_bundle(bundle_name: &str) -> Vec<String> {
let registry = stream_registry();
let Ok(entries) = registry.entries.lock() else {
return Vec::new();
};
entries
.iter()
.filter_map(|(key, entry)| {
if entry.session_type != SessionType::Ui {
return None;
}
entry.writer.as_ref()?;
match key {
RegistryKey::Session {
bundle_name: entry_bundle,
session_id,
} if entry_bundle == bundle_name => Some(session_id.clone()),
RegistryKey::RelayWide { principal_id } => Some(principal_id.clone()),
RegistryKey::Session { .. } => None,
}
})
.collect()
}
pub(super) fn list_registered_relay_wide_sessions() -> Vec<(String, SessionType)> {
let registry = stream_registry();
let Ok(entries) = registry.entries.lock() else {
return Vec::new();
};
entries
.iter()
.filter_map(|(key, entry)| {
entry.writer.as_ref()?;
match key {
RegistryKey::RelayWide { principal_id } => {
Some((principal_id.clone(), entry.session_type))
}
RegistryKey::Session { .. } => None,
}
})
.collect()
}
pub(super) fn broadcast_event_to_bundle_ui(
bundle_name: &str,
template: &RelayStreamEvent,
) -> Vec<String> {
let ui_session_ids = list_registered_ui_sessions_for_bundle(bundle_name);
let mut delivered = Vec::new();
for ui_session_id in ui_session_ids {
let mut event = template.clone();
event.target_session = ui_session_id.clone();
if matches!(
send_event_to_registered_ui(bundle_name, ui_session_id.as_str(), &event),
Ok(StreamEventSendOutcome::Delivered)
) {
delivered.push(ui_session_id);
}
}
delivered
}
pub(super) fn send_event_to_registered_ui(
bundle_name: &str,
session_id: &str,
event: &RelayStreamEvent,
) -> Result<StreamEventSendOutcome, io::Error> {
let registry = stream_registry();
let key = registry_key_for_target(bundle_name, session_id);
let (session_type, writer) = {
let entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
let Some(entry) = entries.get(&key) else {
return Ok(StreamEventSendOutcome::NoUiEndpoint);
};
(entry.session_type, entry.writer.clone())
};
if session_type != SessionType::Ui {
return Ok(StreamEventSendOutcome::NoUiEndpoint);
}
let Some(writer) = writer else {
return Ok(StreamEventSendOutcome::Disconnected);
};
if write_stream_frame_to_writer(&writer, OutgoingFrame::Event { event }).is_ok() {
return Ok(StreamEventSendOutcome::Delivered);
}
let mut entries = registry
.entries
.lock()
.map_err(|_| io::Error::other("failed to lock stream registry"))?;
if let Some(entry) = entries.get_mut(&key) {
entry.stream_id = None;
entry.writer = None;
entry.revoke = None;
}
Ok(StreamEventSendOutcome::Disconnected)
}
pub(super) fn write_stream_frame_to_writer(
writer: &SharedStreamWriter,
frame: OutgoingFrame<'_>,
) -> Result<(), io::Error> {
let mut bytes = encode_outgoing_frame(frame)?.into_bytes();
bytes.push(b'\n');
enqueue_bytes(writer, bytes)
}
fn enqueue_bytes(writer: &SharedStreamWriter, bytes: Vec<u8>) -> Result<(), io::Error> {
writer.send(bytes).map_err(|SendError(_)| {
io::Error::new(io::ErrorKind::BrokenPipe, "stream writer task has closed")
})
}
fn note_write_timeout(error: &io::Error) {
if matches!(
error.kind(),
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
) {
emit_inscription(
"relay.connection.write_timeout",
&serde_json::json!({ "cause": error.to_string() }),
);
}
}
fn stream_registry() -> &'static StreamRegistry {
STREAM_REGISTRY.get_or_init(StreamRegistry::default)
}