rho-coding-agent 1.17.0

A lightweight agent harness inspired by Pi
Documentation
use rho_sdk::{
    tool::{
        Tool, ToolAccessMode, ToolExecutionPolicy, ToolInvocation, ToolPreparationContext,
        ToolResourceKind,
    },
    CancellationToken, ToolCallId, Workspace,
};
use serde_json::json;
use tempfile::TempDir;

use super::{process, rho, sdk_features, web};

fn invocation(arguments: serde_json::Value) -> ToolInvocation {
    ToolInvocation::new(ToolCallId::from_string("call-1").unwrap(), arguments)
}

fn preparation_context(workspace: Option<Workspace>) -> ToolPreparationContext {
    ToolPreparationContext::new(workspace, CancellationToken::new())
}

fn one_access(
    prepared: &rho_sdk::tool::PreparedToolInvocation<'_>,
) -> (ToolResourceKind, ToolAccessMode) {
    let ToolExecutionPolicy::ResourceAware { accesses } = prepared.execution_policy() else {
        panic!("expected a resource-aware invocation");
    };
    assert_eq!(accesses.len(), 1);
    (accesses[0].resource().kind(), accesses[0].mode())
}

#[tokio::test]
async fn process_actions_prepare_with_audited_policies() {
    let manager = process::ProcessManager::new(process::ProcessLimits::default());
    let tool = process::sdk_process::SdkProcess::new(
        process::Process::new(manager),
        12_000,
        rho_sdk::ProcessEnvironment::InheritAll,
    );

    let start = tool
        .prepare(
            invocation(json!({"action": "start", "command": "echo test"})),
            preparation_context(None),
        )
        .await
        .unwrap();
    assert!(matches!(
        start.execution_policy(),
        ToolExecutionPolicy::Exclusive
    ));

    let poll = tool
        .prepare(
            invocation(json!({"action": "poll", "process_id": "process-1"})),
            preparation_context(None),
        )
        .await
        .unwrap();
    assert_eq!(
        one_access(&poll),
        (ToolResourceKind::ManagedProcess, ToolAccessMode::Shared)
    );

    let stop = tool
        .prepare(
            invocation(json!({"action": "stop", "process_id": "process-1"})),
            preparation_context(None),
        )
        .await
        .unwrap();
    assert_eq!(
        one_access(&stop),
        (ToolResourceKind::ManagedProcess, ToolAccessMode::Exclusive)
    );
}

#[tokio::test]
async fn skill_prepares_builtin_and_file_resources() {
    let tool = sdk_features::SdkSkillTool::new(12_000);
    let root = TempDir::new().unwrap();
    let workspace = Workspace::new(root.path()).unwrap();

    let builtin = tool
        .prepare(
            invocation(json!({"name": "rho-diagnostics"})),
            preparation_context(Some(workspace.clone())),
        )
        .await
        .unwrap();
    assert_eq!(
        one_access(&builtin),
        (ToolResourceKind::Opaque, ToolAccessMode::Shared)
    );
    assert_eq!(builtin.capabilities().len(), 1);

    let skill_dir = root.path().join(".agents/skills/prepared-skill");
    std::fs::create_dir_all(&skill_dir).unwrap();
    std::fs::write(
        skill_dir.join("SKILL.md"),
        "---\nname: prepared-skill\ndescription: prepared test\n---\ncontents\n",
    )
    .unwrap();
    let file = tool
        .prepare(
            invocation(json!({"name": "prepared-skill"})),
            preparation_context(Some(workspace)),
        )
        .await
        .unwrap();
    assert_eq!(
        one_access(&file),
        (ToolResourceKind::WorkspacePath, ToolAccessMode::Shared)
    );
    assert_eq!(file.capabilities().len(), 1);
}

#[tokio::test]
async fn preparation_rejects_invalid_resource_keys_and_actions() {
    let manager = process::ProcessManager::new(process::ProcessLimits::default());
    let process = process::sdk_process::SdkProcess::new(
        process::Process::new(manager),
        12_000,
        rho_sdk::ProcessEnvironment::InheritAll,
    );
    let process_error = process
        .prepare(
            invocation(json!({
                "action": "poll",
                "process_id": "process-1",
                "wait_seconds": 31
            })),
            preparation_context(None),
        )
        .await
        .err()
        .unwrap();
    assert_eq!(
        process_error.kind(),
        rho_sdk::tool::ToolErrorKind::InvalidArguments
    );

    let diagnostics = rho::SdkRho::new(crate::diagnostics::test_diagnostics("openai", "gpt-test"));
    let diagnostics_error = diagnostics
        .prepare(
            invocation(json!({"action": "mutate"})),
            preparation_context(None),
        )
        .await
        .err()
        .unwrap();
    assert_eq!(
        diagnostics_error.kind(),
        rho_sdk::tool::ToolErrorKind::InvalidArguments
    );

    let get_search_content =
        web::sdk_get_search_content::SdkGetSearchContent::new(12_000, web::WebAccessStore::new());
    let response_error = get_search_content
        .prepare(
            invocation(json!({"responseId": "../not-a-response"})),
            preparation_context(None),
        )
        .await
        .err()
        .unwrap();
    assert_eq!(
        response_error.kind(),
        rho_sdk::tool::ToolErrorKind::InvalidArguments
    );
}

#[tokio::test]
async fn diagnostics_and_response_store_prepare_as_safe_reads() {
    let diagnostics = rho::SdkRho::new(crate::diagnostics::test_diagnostics("openai", "gpt-test"));
    let prepared = diagnostics
        .prepare(
            invocation(json!({"action": "info"})),
            preparation_context(None),
        )
        .await
        .unwrap();
    let ToolExecutionPolicy::ResourceAware { accesses } = prepared.execution_policy() else {
        panic!("expected resource-aware diagnostics");
    };
    assert!(accesses.is_empty());

    let get_search_content =
        web::sdk_get_search_content::SdkGetSearchContent::new(12_000, web::WebAccessStore::new());
    let stored = get_search_content
        .prepare(
            invocation(json!({"responseId": "0123456789abcdef0123456789abcdef"})),
            preparation_context(None),
        )
        .await
        .unwrap();
    assert_eq!(
        one_access(&stored),
        (ToolResourceKind::ResponseStore, ToolAccessMode::Shared)
    );
}

#[tokio::test]
async fn web_search_prepares_tool_managed_network() {
    use rho_sdk::{CapabilityKind, CapabilityOperation, CapabilitySource, NetworkTarget};

    let tool = web::SdkWebSearch::new(
        web::access_tools_with_store(
            &crate::config::Config::default(),
            web::WebAccessStore::new(),
        ),
        12_000,
    );
    let prepared = tool
        .prepare(
            invocation(json!({"queries": ["rho web search"]})),
            preparation_context(None),
        )
        .await
        .unwrap();
    let ToolExecutionPolicy::ResourceAware { accesses } = prepared.execution_policy() else {
        panic!("expected resource-aware web_search");
    };
    assert!(accesses.is_empty());
    assert_eq!(prepared.capabilities().len(), 1);
    let capability = &prepared.capabilities()[0];
    assert_eq!(capability.kind(), CapabilityKind::Network);
    assert!(matches!(
        capability.operation(),
        CapabilityOperation::NetworkAccess(NetworkTarget::ToolManaged)
    ));
    assert_eq!(
        capability.source(),
        &CapabilitySource::built_in_tool("web_search")
    );
}