rho-coding-agent 1.0.4

A lightweight agent harness inspired by Pi
Documentation
use rho_sdk::{
    tool::{ToolContext, ToolError, ToolErrorKind, ToolSecurity},
    CapabilityKind, CapabilityRequest, CapabilitySource, NetworkTarget, ProcessEnvironment,
    ProcessExecution, ProcessInvocation, ProcessOutputLimits,
};

use super::sdk_support::{required_string, workspace};

pub(super) fn security_for(name: &str) -> ToolSecurity {
    let capabilities = match name {
        "process" => vec![CapabilityKind::Process],
        "web_search" => vec![CapabilityKind::Network],
        "skill" => vec![CapabilityKind::Skill],
        _ => Vec::new(),
    };
    ToolSecurity::built_in(capabilities)
}

pub(super) async fn authorize_builtin(
    name: &str,
    arguments: &serde_json::Value,
    context: &ToolContext,
    max_output_bytes: usize,
) -> Result<(), ToolError> {
    let source = CapabilitySource::built_in_tool(name);
    match name {
        "process"
            if arguments.get("action").and_then(serde_json::Value::as_str) == Some("start") =>
        {
            let command = required_string(arguments, "command")?;
            authorize_process(
                context,
                ProcessInvocation::shell_from_path(shell_executable(), shell_arguments(), command),
                optional_timeout(arguments)?,
                max_output_bytes,
                source,
            )
            .await
        }
        "web_search" => {
            authorize_request(
                context,
                CapabilityRequest::network(NetworkTarget::ToolManaged, source),
            )
            .await
        }
        "skill" => {
            let name = required_string(arguments, "name")?;
            authorize_request(context, CapabilityRequest::skill(name, None, source)).await
        }
        _ => Ok(()),
    }
}

pub(super) async fn authorize_request(
    context: &ToolContext,
    request: CapabilityRequest,
) -> Result<(), ToolError> {
    context
        .authorize(request)
        .await
        .map(|_| ())
        .map_err(|error| {
            if error.kind() == rho_sdk::AuthorizationDenialKind::Cancelled {
                ToolError::cancelled()
            } else {
                ToolError::policy_denied(&error)
            }
        })
}

async fn authorize_process(
    context: &ToolContext,
    invocation: ProcessInvocation,
    timeout: Option<std::time::Duration>,
    max_output_bytes: usize,
    source: CapabilitySource,
) -> Result<(), ToolError> {
    let workspace = workspace(context)?;
    let cwd = workspace
        .resolve_for_read(workspace.root())
        .map_err(|error| ToolError::new(ToolErrorKind::PolicyDenied, error.to_string()))?;
    let execution = ProcessExecution::new(
        cwd.path(),
        invocation,
        ProcessEnvironment::InheritAll,
        ProcessOutputLimits::new(max_output_bytes, timeout),
    );
    authorize_request(context, CapabilityRequest::process(execution, source)).await?;
    workspace
        .revalidate(&cwd)
        .map_err(|error| ToolError::new(ToolErrorKind::PolicyDenied, error.to_string()))
}

fn optional_timeout(
    arguments: &serde_json::Value,
) -> Result<Option<std::time::Duration>, ToolError> {
    arguments
        .get("timeout_seconds")
        .map(|value| {
            value
                .as_u64()
                .filter(|seconds| *seconds > 0)
                .map(std::time::Duration::from_secs)
                .ok_or_else(|| {
                    ToolError::new(
                        ToolErrorKind::InvalidArguments,
                        "timeout_seconds must be a positive integer",
                    )
                })
        })
        .transpose()
}

#[cfg(unix)]
fn shell_executable() -> &'static str {
    "bash"
}

#[cfg(windows)]
fn shell_executable() -> &'static str {
    "powershell.exe"
}

#[cfg(unix)]
fn shell_arguments() -> Vec<String> {
    vec!["-lc".into()]
}

#[cfg(windows)]
fn shell_arguments() -> Vec<String> {
    vec![
        "-NoProfile".into(),
        "-NonInteractive".into(),
        "-Command".into(),
    ]
}