rho-coding-agent 1.48.0

A lightweight agent harness inspired by Pi
use std::sync::atomic::{AtomicU64, Ordering};

use agent_client_protocol::schema::v1::{
    PermissionOption, PermissionOptionKind, RequestPermissionOutcome, RequestPermissionRequest,
    SessionId, SessionMode, SessionModeId, ToolCallId, ToolCallUpdate, ToolCallUpdateFields,
    ToolKind,
};

use crate::permission::PermissionMode;
use rho_sdk::CapabilityKind;

const OPTION_ALLOW_ONCE: &str = "allow_once";
const OPTION_ALLOW_ALWAYS: &str = "allow_always";
const OPTION_REJECT_ONCE: &str = "reject_once";

pub(super) fn mode_list() -> Vec<SessionMode> {
    PermissionMode::ALL
        .into_iter()
        .map(|mode| SessionMode::new(mode_id(mode), mode.label()))
        .collect()
}

pub(super) fn mode_id(mode: PermissionMode) -> SessionModeId {
    SessionModeId::new(mode.as_str())
}

pub(super) fn parse_mode_id(id: &str) -> Option<PermissionMode> {
    PermissionMode::ALL
        .into_iter()
        .find(|mode| mode.as_str() == id)
}

pub(super) fn next_placeholder_tool_call_id(counter: &AtomicU64) -> String {
    format!("pending-{}", counter.fetch_add(1, Ordering::Relaxed) + 1)
}

pub(super) fn permission_request(
    session_id: &SessionId,
    request: &rho_sdk::ApprovalRequest,
    placeholder_tool_call_id: &str,
) -> RequestPermissionRequest {
    let tool_call_id = request
        .tool_call_id()
        .map(|id| id.as_str().to_string())
        .unwrap_or_else(|| placeholder_tool_call_id.to_string());
    RequestPermissionRequest::new(
        session_id.clone(),
        ToolCallUpdate::new(
            ToolCallId::new(tool_call_id),
            ToolCallUpdateFields::new()
                .title(request.reason())
                .kind(tool_kind_for_capability(request.capability().kind())),
        ),
        permission_options(),
    )
}

pub(super) fn decision_for(outcome: &RequestPermissionOutcome) -> rho_sdk::ApprovalDecision {
    match outcome {
        RequestPermissionOutcome::Cancelled => rho_sdk::ApprovalDecision::Deny {
            reason: "permission request cancelled".into(),
        },
        RequestPermissionOutcome::Selected(selected) => match selected.option_id.0.as_ref() {
            OPTION_ALLOW_ONCE => rho_sdk::ApprovalDecision::AllowOnce,
            OPTION_ALLOW_ALWAYS => rho_sdk::ApprovalDecision::AllowForSession,
            OPTION_REJECT_ONCE => rho_sdk::ApprovalDecision::Deny {
                reason: "permission rejected".into(),
            },
            _ => rho_sdk::ApprovalDecision::Deny {
                reason: "unknown permission option".into(),
            },
        },
        _ => rho_sdk::ApprovalDecision::Deny {
            reason: "unknown permission outcome".into(),
        },
    }
}

fn permission_options() -> Vec<PermissionOption> {
    vec![
        PermissionOption::new(
            OPTION_ALLOW_ONCE,
            "Allow once",
            PermissionOptionKind::AllowOnce,
        ),
        PermissionOption::new(
            OPTION_ALLOW_ALWAYS,
            "Allow for this session",
            PermissionOptionKind::AllowAlways,
        ),
        PermissionOption::new(
            OPTION_REJECT_ONCE,
            "Reject once",
            PermissionOptionKind::RejectOnce,
        ),
    ]
}

fn tool_kind_for_capability(kind: CapabilityKind) -> ToolKind {
    match kind {
        CapabilityKind::Read | CapabilityKind::InstructionDiscovery => ToolKind::Read,
        CapabilityKind::Write => ToolKind::Edit,
        CapabilityKind::Process => ToolKind::Execute,
        CapabilityKind::Network => ToolKind::Fetch,
        CapabilityKind::Skill => ToolKind::Other,
        _ => ToolKind::Other,
    }
}

#[cfg(test)]
#[path = "permission_tests.rs"]
mod tests;