harn-vm 0.10.23

Async bytecode virtual machine for the Harn programming language
Documentation
use super::{agent_primitive_denied_tool, host_agent_dispatch_tool_call};

#[test]
fn approval_unavailable_names_terminal_risk_class() {
    use crate::agent_events::{DenialGate, ToolCallErrorCategory, ToolDenial};

    let denial = ToolDenial::terminal(
        DenialGate::ApprovalUnavailable,
        None,
        "approval required but no host bridge is available",
    )
    .with_denial_class("approval_required+package_install", 2);
    let envelope = agent_primitive_denied_tool(
        "exec",
        "call_8",
        &serde_json::json!({ "command": "pip install pytest" }),
        denial.reason.clone(),
        ToolCallErrorCategory::PermissionDenied,
        Some(&denial),
        None,
    );
    let result = &envelope["result"];
    assert_eq!(result["error"], serde_json::json!("permission_denied"));
    assert_eq!(
        result["denial_class"],
        serde_json::json!("approval_required+package_install")
    );
    assert_eq!(result["class_repeat_count"], serde_json::json!(2));
    let next = result["next_step"].as_str().expect("next_step");
    assert!(
        next.contains("already marked terminal"),
        "repeated class denial should not invite more variants: {next}"
    );
    assert!(
        next.contains("approval_required+package_install"),
        "next_step should name the terminal risk class: {next}"
    );
    assert_eq!(
        result["denial"]["denial_class"],
        serde_json::json!("approval_required+package_install")
    );
    assert_eq!(result["denial"]["class_repeat_count"], serde_json::json!(2));
}

#[tokio::test]
async fn approval_unavailable_denials_share_terminal_risk_class_across_arg_variants() {
    crate::orchestration::clear_execution_policy_stacks();
    crate::orchestration::clear_all_approval_policy_repeat_counts();
    let policy: crate::orchestration::ToolApprovalPolicy =
        serde_json::from_value(serde_json::json!({
            "rules": [{
                "ask": {"tool": "exec", "command_identity": "pip"},
                "reason": "package installs require approval"
            }]
        }))
        .expect("approval policy");
    crate::orchestration::push_approval_policy(policy);

    let mut options = crate::value::DictMap::new();
    options.insert(
        crate::value::intern_key("session_id"),
        crate::stdlib::json_to_vm_value(&serde_json::json!(
            "approval-unavailable-terminal-class-test"
        )),
    );

    let first = dispatch_exec("pip install pytest", &options).await;
    let second = dispatch_exec("pip install requests --break-system-packages", &options).await;

    crate::orchestration::pop_approval_policy();
    crate::orchestration::clear_all_approval_policy_repeat_counts();

    assert_eq!(
        first["result"]["error"],
        serde_json::json!("permission_denied")
    );
    assert_eq!(
        first["result"]["denial"]["gate"],
        serde_json::json!("approval_unavailable")
    );
    assert_eq!(
        first["result"]["denial_class"],
        serde_json::json!("approval_required+command_rule")
    );
    assert_eq!(first["result"]["class_repeat_count"], serde_json::json!(1));
    assert_eq!(
        second["result"]["denial_class"],
        serde_json::json!("approval_required+command_rule")
    );
    assert_eq!(
        second["result"]["class_repeat_count"],
        serde_json::json!(2),
        "same session + same policy risk class should be repeat-counted even when args differ"
    );
    let next_step = second["result"]["next_step"].as_str().expect("next_step");
    assert!(
        next_step.contains("already marked terminal"),
        "repeat feedback must suppress approval-required arg churn: {next_step}"
    );
}

async fn dispatch_exec(command: &str, options: &crate::value::DictMap) -> serde_json::Value {
    let call = crate::stdlib::json_to_vm_value(&serde_json::json!({
        "id": format!("exec-{command}"),
        "name": "exec",
        "arguments": {"command": command},
    }));
    let result = host_agent_dispatch_tool_call(
        crate::vm::AsyncBuiltinCtx::for_test(crate::vm::Vm::new()),
        call,
        None,
        options,
    )
    .await
    .expect("approval-unavailable denial is a normal dispatch result");
    crate::llm::helpers::vm_value_to_json(&result)
}