harn-vm 0.10.52

Async bytecode virtual machine for the Harn programming language
Documentation
use std::collections::BTreeMap;

use serde::Serialize;

use crate::orchestration::CapabilityPolicy;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConnectorExportEffectClass {
    HotPathLocal,
    ConnectorOutbound,
    Activation,
}

#[derive(Clone, Debug, Default)]
pub struct HarnConnectorEffectPolicies {
    overrides: BTreeMap<String, Option<CapabilityPolicy>>,
}

impl HarnConnectorEffectPolicies {
    pub fn set_export_policy(
        &mut self,
        export: impl Into<String>,
        policy: CapabilityPolicy,
    ) -> &mut Self {
        self.overrides.insert(export.into(), Some(policy));
        self
    }

    pub fn trust_export(&mut self, export: impl Into<String>) -> &mut Self {
        self.overrides.insert(export.into(), None);
        self
    }

    pub fn clear_export_override(&mut self, export: &str) -> &mut Self {
        self.overrides.remove(export);
        self
    }

    pub(crate) fn policy_for_export(&self, export: &str) -> Option<CapabilityPolicy> {
        self.overrides
            .get(export)
            .cloned()
            .unwrap_or_else(|| default_connector_export_policy(export))
    }
}

pub fn connector_export_effect_class(export: &str) -> Option<ConnectorExportEffectClass> {
    match export {
        "normalize_inbound" => Some(ConnectorExportEffectClass::HotPathLocal),
        "poll_tick" | "call" => Some(ConnectorExportEffectClass::ConnectorOutbound),
        "activate" => Some(ConnectorExportEffectClass::Activation),
        _ => None,
    }
}

pub fn default_connector_export_policy(export: &str) -> Option<CapabilityPolicy> {
    let class = connector_export_effect_class(export)?;
    Some(policy_for_effect_class(class))
}

pub fn connector_export_denied_builtin_reason(export: &str, builtin: &str) -> Option<&'static str> {
    let class = connector_export_effect_class(export)?;
    match builtin_effect_group(builtin)? {
        BuiltinEffectGroup::Workspace => Some("ambient filesystem access is not allowed"),
        BuiltinEffectGroup::Process => Some("process execution is not allowed"),
        BuiltinEffectGroup::Llm => Some("LLM calls are not allowed"),
        BuiltinEffectGroup::Mcp => Some("MCP/process-backed connector access is not allowed"),
        BuiltinEffectGroup::Host => Some("host calls require an explicit host-owned surface"),
        BuiltinEffectGroup::Network | BuiltinEffectGroup::ConnectorCall => match class {
            ConnectorExportEffectClass::HotPathLocal => {
                Some("outbound network/client calls are not allowed on the ingress hot path")
            }
            ConnectorExportEffectClass::ConnectorOutbound
            | ConnectorExportEffectClass::Activation => None,
        },
    }
}

/// Return an actionable lint reason when a typed Harness method exceeds an
/// export's default ceiling. Both the contract effects and the ceiling are the
/// same values used by runtime enforcement; the linter does not maintain a
/// second capability-name table.
pub fn connector_export_denied_harness_method_reason(
    export: &str,
    capability_field: &str,
    method: &str,
) -> Option<String> {
    let policy = default_connector_export_policy(export)?;
    let capability = harn_builtin_meta::CapabilityId::from_field_name(capability_field)?;
    let entry = crate::stdlib::capability_method_manifest_entry(capability, method)?;
    let denied = crate::orchestration::runtime_effects_from_contract(entry.contract.effects, &[])
        .into_iter()
        .find(|effect| !crate::orchestration::effect_allowed_by_ceiling(effect, &policy))?;
    Some(format!(
        "{} is outside the `{export}` default effect ceiling",
        crate::orchestration::effect_record_summary(&denied)
    ))
}

fn policy_for_effect_class(class: ConnectorExportEffectClass) -> CapabilityPolicy {
    let mut capabilities = BTreeMap::from([
        ("secrets".to_string(), vec!["read".to_string()]),
        ("observability".to_string(), vec!["emit".to_string()]),
        ("clock".to_string(), vec!["now".to_string()]),
        ("state".to_string(), vec!["read".to_string()]),
    ]);
    if matches!(
        class,
        ConnectorExportEffectClass::ConnectorOutbound | ConnectorExportEffectClass::Activation
    ) {
        capabilities.insert("network".to_string(), vec!["http".to_string()]);
        capabilities
            .entry("state".to_string())
            .or_default()
            .push("write".to_string());
    }

    CapabilityPolicy {
        capabilities,
        side_effect_level: Some(match class {
            ConnectorExportEffectClass::HotPathLocal => "read_only".to_string(),
            ConnectorExportEffectClass::ConnectorOutbound
            | ConnectorExportEffectClass::Activation => "network".to_string(),
        }),
        ..CapabilityPolicy::default()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BuiltinEffectGroup {
    Workspace,
    Process,
    Network,
    Llm,
    Mcp,
    Host,
    ConnectorCall,
}

fn builtin_effect_group(builtin: &str) -> Option<BuiltinEffectGroup> {
    match builtin {
        "read_file"
        | "read_file_result"
        | "read_file_bytes"
        | "package_snapshot_open"
        | "render"
        | "render_prompt"
        | "render_with_provenance"
        | "write_file"
        | "write_file_bytes"
        | "replace_file"
        | "replace_file_result"
        | "replace_file_bytes"
        | "replace_file_bytes_result"
        | "append_file"
        | "append_file_locked"
        | "copy_file"
        | "delete_file"
        | "mkdir"
        | "list_dir"
        | "file_exists"
        | "stat"
        | "project_fingerprint"
        | "project_context_profile_native"
        | "project_scan_native"
        | "project_scan_tree_native"
        | "project_walk_tree_native"
        | "project_catalog_native"
        | "__agent_state_init"
        | "__agent_state_resume"
        | "__agent_state_write"
        | "__agent_state_read"
        | "__agent_state_list"
        | "__agent_state_delete"
        | "__agent_state_handoff" => Some(BuiltinEffectGroup::Workspace),
        "exec" | "exec_at" | "shell" | "shell_at" => Some(BuiltinEffectGroup::Process),
        "http_get"
        | "http_post"
        | "http_put"
        | "http_patch"
        | "http_delete"
        | "http_download"
        | "http_request"
        | "http_session_request"
        | "http_stream_open"
        | "http_stream_read"
        | "http_stream_close"
        | "http_stream_info"
        | "sse_connect"
        | "sse_receive"
        | "websocket_accept"
        | "websocket_connect"
        | "websocket_route"
        | "websocket_send"
        | "websocket_receive"
        | "websocket_server" => Some(BuiltinEffectGroup::Network),
        "llm_call" | "llm_call_safe" | "llm_completion" | "llm_stream" | "llm_stream_call"
        | "llm_healthcheck" | "agent_loop" => Some(BuiltinEffectGroup::Llm),
        "vision_ocr" => Some(BuiltinEffectGroup::Process),
        "mcp_connect"
        | "mcp_ensure_active"
        | "mcp_call"
        | "mcp_list_tools"
        | "mcp_list_resources"
        | "mcp_list_resource_templates"
        | "mcp_read_resource"
        | "mcp_list_prompts"
        | "mcp_get_prompt"
        | "mcp_server_info"
        | "mcp_disconnect" => Some(BuiltinEffectGroup::Mcp),
        "host_call" | "host_tool_call" | "host_tool_list" => Some(BuiltinEffectGroup::Host),
        "connector_call" => Some(BuiltinEffectGroup::ConnectorCall),
        _ => None,
    }
}