harn-vm 0.10.35

Async bytecode virtual machine for the Harn programming language
Documentation
use serde_json::{json, Value as JsonValue};
use uuid::Uuid;

use crate::value::VmError;

use super::{display_path, GitCommand, GitMutation};

pub(super) async fn enforce_git_approval(
    ctx: Option<&crate::vm::AsyncBuiltinCtx>,
    command: &GitCommand,
) -> Result<Option<JsonValue>, VmError> {
    let args = json!({
        "operation": command.operation,
        "argv": command.argv,
        "cwd": display_path(&command.cwd),
        "affected_paths": command.affected_paths,
    });
    let policy_decision = crate::orchestration::current_approval_policy()
        .map(|policy| policy.evaluate_detailed(command.operation, &args));
    if let Some(decision) = policy_decision
        .as_ref()
        .filter(|decision| decision.is_deny())
    {
        return Err(VmError::CategorizedError {
            message: decision.reason.clone(),
            category: crate::value::ErrorCategory::ToolRejected,
        });
    }
    if command.mutation == GitMutation::Risky {
        if let Some(approval) = crate::orchestration::current_operator_approval_grant()
            .and_then(|grant| grant.receipt_for(command.operation))
        {
            return Ok(Some(approval));
        }
        return request_permission(
            ctx,
            command.operation,
            &args,
            policy_decision.map(|decision| decision.receipt),
        )
        .await
        .map(Some);
    }
    let Some(decision) = policy_decision else {
        return Ok(None);
    };
    if decision.is_allow() {
        Ok(None)
    } else {
        request_permission(ctx, command.operation, &args, Some(decision.receipt))
            .await
            .map(Some)
    }
}

async fn request_permission(
    ctx: Option<&crate::vm::AsyncBuiltinCtx>,
    operation: &str,
    args: &JsonValue,
    policy_decision: Option<JsonValue>,
) -> Result<JsonValue, VmError> {
    let Some(bridge) = ctx.and_then(|ctx| ctx.child_vm().bridge.clone()) else {
        return Err(VmError::CategorizedError {
            message: format!("{operation}: approval required but no host bridge is attached"),
            category: crate::value::ErrorCategory::ToolRejected,
        });
    };
    let approval_id = format!("git-{}", Uuid::now_v7());
    let approval_request = crate::stdlib::hitl::approval_request_for_host_permission(
        approval_id.clone(),
        operation.to_string(),
        args.clone(),
        crate::llm::current_agent_session_id().unwrap_or_else(|| "harn".to_string()),
        Vec::new(),
        policy_decision
            .as_ref()
            .map(|decision| json!({"policy_decision": decision}))
            .unwrap_or(JsonValue::Null),
        vec![format!("stdlib.{operation}")],
    );
    let approval_request_json = serde_json::to_value(&approval_request).unwrap_or(JsonValue::Null);
    let response = bridge
        .call(
            crate::llm::acp_permission::METHOD_REQUEST_PERMISSION,
            crate::llm::acp_permission::request_params(
                crate::llm::current_agent_session_id().as_deref(),
                &approval_id,
                operation,
                args,
                approval_request_json,
                &policy_decision.clone().unwrap_or(JsonValue::Null),
                None,
                crate::tool_annotations::ToolKind::Other,
            ),
        )
        .await?;
    match crate::llm::acp_permission::parse_response(&response) {
        crate::llm::acp_permission::WireOutcome::Allowed => Ok(response),
        crate::llm::acp_permission::WireOutcome::Rejected { reason } => {
            Err(VmError::CategorizedError {
                message: format!("{operation}: approval denied: {reason}"),
                category: crate::value::ErrorCategory::ToolRejected,
            })
        }
    }
}