agentmux 0.6.0

Multi-agent coordination runtime with inter-agent messaging across CLI, MCP, tmux, and ACP.
Documentation
use std::path::Path;

use serde_json::json;

use crate::configuration::{BundleConfiguration, load_bundle_configuration};

use super::super::authorization::{
    AuthorizationContext, authorize_grant, authorize_grant_for_list, grant_authorized_ui_sessions,
    load_authorization_context,
};
use super::super::delivery::{
    PermissionDecisionKind, PermissionDecisionRequest, PermissionEventContext,
    PermissionResolutionOutcome, PersistedPendingPermissionRequest,
    emit_permission_snapshot_then_replay, list_pending_permission_requests,
    map_permission_state_error, resolve_permission_request,
};
use super::super::{
    PendingPermissionEntry, PermissionDecisionRequestContext, RelayError, RelayResponse,
    RequestPrincipal, SCHEMA_VERSION, canonical_session_id, map_config, relay_error,
};

pub(super) fn emit_permission_snapshot_for_ui_registration(
    configuration_root: &Path,
    bundle_name: &str,
    runtime_directory: &Path,
    ui_session_id: &str,
) -> Result<(), RelayError> {
    let bundle = load_bundle_configuration(configuration_root, bundle_name).map_err(map_config)?;
    let authorization = load_authorization_context(configuration_root, &bundle)?;
    let authorized_sessions = grant_authorized_ui_sessions(&authorization, &bundle);
    if !authorized_sessions
        .iter()
        .any(|value| value == ui_session_id)
    {
        return Ok(());
    }
    let context = PermissionEventContext {
        runtime_directory: runtime_directory.to_path_buf(),
        bundle_name: bundle.bundle_name.clone(),
        authorized_ui_sessions: authorized_sessions,
    };
    emit_permission_snapshot_then_replay(&context, ui_session_id).map_err(|cause| {
        let mut error = map_permission_state_error(
            "runtime_permission_queue_unavailable",
            "failed to replay permission snapshot for ui session",
        );
        error.details = Some(json!({
            "bundle_name": bundle.bundle_name,
            "session_id": ui_session_id,
            "cause": cause,
        }));
        error
    })
}

pub(super) fn handle_permission_decision(
    bundle: &BundleConfiguration,
    authorization: &AuthorizationContext,
    request: PermissionDecisionRequestContext,
    runtime_directory: &Path,
    principal: Option<RequestPrincipal>,
) -> Result<RelayResponse, RelayError> {
    let PermissionDecisionRequestContext {
        permission_request_id,
        outcome,
        option_id,
        bundle_name: request_bundle_name,
        ui_session_id,
    } = request;
    validate_permission_decision_request(
        bundle,
        request_bundle_name.as_deref(),
        ui_session_id.as_ref(),
        permission_request_id.as_str(),
        option_id.as_deref(),
    )?;
    let decision = permission_decision_kind(outcome.as_str(), option_id.as_ref())?;
    let principal = principal.ok_or_else(|| {
        relay_error(
            "validation_missing_hello",
            "permission decisions require stream-associated principal identity",
            None,
        )
    })?;
    authorize_grant(
        bundle,
        authorization,
        principal.session_id.as_str(),
        permission_request_id.as_str(),
    )?;
    let context = PermissionEventContext {
        runtime_directory: runtime_directory.to_path_buf(),
        bundle_name: bundle.bundle_name.clone(),
        authorized_ui_sessions: grant_authorized_ui_sessions(authorization, bundle),
    };
    let outcome = resolve_permission_request(
        &context,
        PermissionDecisionRequest {
            permission_request_id: permission_request_id.clone(),
            option_id: option_id.clone(),
            decision,
            decided_by: canonical_session_id(
                principal.session_id.as_str(),
                bundle.bundle_name.as_str(),
            ),
        },
    )
    .map_err(|cause| map_permission_resolution_error(cause, &permission_request_id, option_id))?;
    let (outcome_label, reason_code, reason_message) = match outcome {
        PermissionResolutionOutcome::Selected { .. } => ("selected".to_string(), None, None),
        PermissionResolutionOutcome::Cancelled {
            reason_code,
            reason,
            ..
        } => ("cancelled".to_string(), Some(reason_code), reason),
    };
    Ok(RelayResponse::PermissionDecision {
        schema_version: SCHEMA_VERSION.to_string(),
        status: "resolved".to_string(),
        permission_request_id,
        outcome: outcome_label,
        reason_code,
        reason: reason_message,
    })
}

pub(super) fn handle_permission_list(
    bundle: &BundleConfiguration,
    authorization: &AuthorizationContext,
    request_bundle_name: Option<String>,
    runtime_directory: &Path,
    principal: Option<RequestPrincipal>,
) -> Result<RelayResponse, RelayError> {
    if let Some(request_bundle_name) = request_bundle_name.as_deref()
        && request_bundle_name != bundle.bundle_name
    {
        return Err(relay_error(
            "validation_cross_bundle_unsupported",
            "permission list is limited to the associated bundle",
            Some(json!({
                "associated_bundle_name": bundle.bundle_name,
                "requested_bundle_name": request_bundle_name,
            })),
        ));
    }
    let principal = principal.ok_or_else(|| {
        relay_error(
            "validation_missing_hello",
            "permission list requires stream-associated principal identity",
            None,
        )
    })?;
    authorize_grant_for_list(bundle, authorization, principal.session_id.as_str())?;
    let pending = list_pending_permission_requests(runtime_directory).map_err(|cause| {
        if cause.starts_with("runtime_permission_queue_unavailable") {
            relay_error(
                "runtime_permission_queue_unavailable",
                "permission queue state is unavailable",
                None,
            )
        } else {
            relay_error(
                "internal_unexpected_failure",
                "failed to list pending permission requests",
                Some(json!({ "cause": cause })),
            )
        }
    })?;
    let pending_requests = pending
        .into_iter()
        .map(|record| {
            let mut entry = pending_permission_entry_from_record(record);
            entry.target_session =
                canonical_session_id(entry.target_session.as_str(), bundle.bundle_name.as_str());
            entry
        })
        .collect();
    Ok(RelayResponse::PermissionList {
        schema_version: SCHEMA_VERSION.to_string(),
        bundle_name: bundle.bundle_name.clone(),
        pending_requests,
    })
}

fn validate_permission_decision_request(
    bundle: &BundleConfiguration,
    request_bundle_name: Option<&str>,
    ui_session_id: Option<&String>,
    permission_request_id: &str,
    option_id: Option<&str>,
) -> Result<(), RelayError> {
    if let Some(request_bundle_name) = request_bundle_name
        && request_bundle_name != bundle.bundle_name
    {
        return Err(relay_error(
            "validation_cross_bundle_unsupported",
            "permission decisions are limited to the associated bundle in MVP",
            Some(json!({
                "associated_bundle_name": bundle.bundle_name,
                "requested_bundle_name": request_bundle_name,
            })),
        ));
    }
    if ui_session_id.is_some() {
        return Err(relay_error(
            "validation_invalid_params",
            "caller-supplied ui_session_id is not allowed",
            Some(json!({"field": "ui_session_id"})),
        ));
    }
    if permission_request_id.trim().is_empty() {
        return Err(relay_error(
            "validation_invalid_params",
            "permission_request_id must be non-empty",
            Some(json!({"field": "permission_request_id"})),
        ));
    }
    if let Some(option_id) = option_id
        && option_id.trim().is_empty()
    {
        return Err(relay_error(
            "validation_invalid_params",
            "option_id must be non-empty when provided",
            Some(json!({"field": "option_id"})),
        ));
    }
    Ok(())
}

fn permission_decision_kind(
    outcome: &str,
    option_id: Option<&String>,
) -> Result<PermissionDecisionKind, RelayError> {
    match outcome {
        "selected" => {
            if option_id.is_none() {
                return Err(relay_error(
                    "validation_invalid_params",
                    "selected outcome requires explicit option_id",
                    Some(json!({"field": "option_id", "outcome": "selected"})),
                ));
            }
            Ok(PermissionDecisionKind::Selected)
        }
        "cancelled" => {
            if option_id.is_some() {
                return Err(relay_error(
                    "validation_invalid_params",
                    "cancelled outcome must omit option_id",
                    Some(json!({"field": "option_id", "outcome": "cancelled"})),
                ));
            }
            Ok(PermissionDecisionKind::Cancelled)
        }
        _ => Err(relay_error(
            "validation_invalid_params",
            "permission decision outcome must be selected or cancelled",
            Some(json!({"field": "outcome", "value": outcome})),
        )),
    }
}

fn map_permission_resolution_error(
    cause: String,
    permission_request_id: &str,
    option_id: Option<String>,
) -> RelayError {
    if cause == "runtime_permission_request_already_resolved" {
        relay_error(
            "runtime_permission_request_already_resolved",
            "permission request is already resolved",
            Some(json!({"permission_request_id": permission_request_id})),
        )
    } else if cause.starts_with("runtime_permission_queue_unavailable") {
        relay_error(
            "runtime_permission_queue_unavailable",
            "permission queue state is unavailable",
            Some(json!({"permission_request_id": permission_request_id})),
        )
    } else if cause.starts_with("validation_invalid_params:") {
        let validation_message = cause
            .split_once(':')
            .map(|(_, message)| message.trim().to_string())
            .unwrap_or_else(|| "invalid permission decision parameters".to_string());
        relay_error(
            "validation_invalid_params",
            validation_message.as_str(),
            Some(json!({"field": "option_id", "value": option_id})),
        )
    } else {
        relay_error(
            "internal_unexpected_failure",
            "failed to resolve permission request",
            Some(json!({
                "permission_request_id": permission_request_id,
                "cause": cause,
            })),
        )
    }
}

fn pending_permission_entry_from_record(
    record: PersistedPendingPermissionRequest,
) -> PendingPermissionEntry {
    let PersistedPendingPermissionRequest {
        message_id,
        permission_request_id,
        target_session,
        requested_kind,
        requested_details,
        enqueued_at,
        ..
    } = record;
    PendingPermissionEntry {
        message_id,
        permission_request_id,
        target_session,
        requested_kind,
        requested_details,
        enqueued_at,
    }
}