Skip to main content

harn_vm/connectors/
effect_policy.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4
5use crate::orchestration::CapabilityPolicy;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ConnectorExportEffectClass {
10    HotPathLocal,
11    ConnectorOutbound,
12    Activation,
13}
14
15#[derive(Clone, Debug, Default)]
16pub struct HarnConnectorEffectPolicies {
17    overrides: BTreeMap<String, Option<CapabilityPolicy>>,
18}
19
20impl HarnConnectorEffectPolicies {
21    pub fn set_export_policy(
22        &mut self,
23        export: impl Into<String>,
24        policy: CapabilityPolicy,
25    ) -> &mut Self {
26        self.overrides.insert(export.into(), Some(policy));
27        self
28    }
29
30    pub fn trust_export(&mut self, export: impl Into<String>) -> &mut Self {
31        self.overrides.insert(export.into(), None);
32        self
33    }
34
35    pub fn clear_export_override(&mut self, export: &str) -> &mut Self {
36        self.overrides.remove(export);
37        self
38    }
39
40    pub(crate) fn policy_for_export(&self, export: &str) -> Option<CapabilityPolicy> {
41        self.overrides
42            .get(export)
43            .cloned()
44            .unwrap_or_else(|| default_connector_export_policy(export))
45    }
46}
47
48pub fn connector_export_effect_class(export: &str) -> Option<ConnectorExportEffectClass> {
49    match export {
50        "normalize_inbound" => Some(ConnectorExportEffectClass::HotPathLocal),
51        "poll_tick" | "call" => Some(ConnectorExportEffectClass::ConnectorOutbound),
52        "activate" => Some(ConnectorExportEffectClass::Activation),
53        _ => None,
54    }
55}
56
57pub fn default_connector_export_policy(export: &str) -> Option<CapabilityPolicy> {
58    let class = connector_export_effect_class(export)?;
59    Some(policy_for_effect_class(class))
60}
61
62pub fn connector_export_denied_builtin_reason(export: &str, builtin: &str) -> Option<&'static str> {
63    let class = connector_export_effect_class(export)?;
64    match builtin_effect_group(builtin)? {
65        BuiltinEffectGroup::Workspace => Some("ambient filesystem access is not allowed"),
66        BuiltinEffectGroup::Process => Some("process execution is not allowed"),
67        BuiltinEffectGroup::Llm => Some("LLM calls are not allowed"),
68        BuiltinEffectGroup::Mcp => Some("MCP/process-backed connector access is not allowed"),
69        BuiltinEffectGroup::Host => Some("host calls require an explicit host-owned surface"),
70        BuiltinEffectGroup::Network | BuiltinEffectGroup::ConnectorCall => match class {
71            ConnectorExportEffectClass::HotPathLocal => {
72                Some("outbound network/client calls are not allowed on the ingress hot path")
73            }
74            ConnectorExportEffectClass::ConnectorOutbound
75            | ConnectorExportEffectClass::Activation => None,
76        },
77    }
78}
79
80fn policy_for_effect_class(class: ConnectorExportEffectClass) -> CapabilityPolicy {
81    let mut capabilities = BTreeMap::new();
82    capabilities.insert(
83        "connector".to_string(),
84        match class {
85            ConnectorExportEffectClass::HotPathLocal => vec![
86                "secret_get".to_string(),
87                "event_log_emit".to_string(),
88                "metrics_inc".to_string(),
89            ],
90            ConnectorExportEffectClass::ConnectorOutbound
91            | ConnectorExportEffectClass::Activation => {
92                vec![
93                    "call".to_string(),
94                    "secret_get".to_string(),
95                    "event_log_emit".to_string(),
96                    "metrics_inc".to_string(),
97                ]
98            }
99        },
100    );
101
102    CapabilityPolicy {
103        capabilities,
104        side_effect_level: Some(match class {
105            ConnectorExportEffectClass::HotPathLocal => "read_only".to_string(),
106            ConnectorExportEffectClass::ConnectorOutbound
107            | ConnectorExportEffectClass::Activation => "network".to_string(),
108        }),
109        ..CapabilityPolicy::default()
110    }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114enum BuiltinEffectGroup {
115    Workspace,
116    Process,
117    Network,
118    Llm,
119    Mcp,
120    Host,
121    ConnectorCall,
122}
123
124fn builtin_effect_group(builtin: &str) -> Option<BuiltinEffectGroup> {
125    match builtin {
126        "read_file"
127        | "read_file_result"
128        | "read_file_bytes"
129        | "write_file"
130        | "write_file_bytes"
131        | "append_file"
132        | "copy_file"
133        | "delete_file"
134        | "mkdir"
135        | "list_dir"
136        | "file_exists"
137        | "stat"
138        | "project_fingerprint"
139        | "project_scan_native"
140        | "project_scan_tree_native"
141        | "project_walk_tree_native"
142        | "project_catalog_native"
143        | "__agent_state_init"
144        | "__agent_state_resume"
145        | "__agent_state_write"
146        | "__agent_state_read"
147        | "__agent_state_list"
148        | "__agent_state_delete"
149        | "__agent_state_handoff" => Some(BuiltinEffectGroup::Workspace),
150        "exec" | "exec_at" | "shell" | "shell_at" => Some(BuiltinEffectGroup::Process),
151        "http_get"
152        | "http_post"
153        | "http_put"
154        | "http_patch"
155        | "http_delete"
156        | "http_request"
157        | "http_session_request"
158        | "sse_connect"
159        | "sse_receive"
160        | "websocket_connect"
161        | "websocket_send"
162        | "websocket_receive" => Some(BuiltinEffectGroup::Network),
163        "llm_call" | "llm_call_safe" | "llm_completion" | "llm_stream" | "llm_healthcheck"
164        | "agent_loop" => Some(BuiltinEffectGroup::Llm),
165        "vision_ocr" => Some(BuiltinEffectGroup::Process),
166        "mcp_connect"
167        | "mcp_ensure_active"
168        | "mcp_call"
169        | "mcp_list_tools"
170        | "mcp_list_resources"
171        | "mcp_list_resource_templates"
172        | "mcp_read_resource"
173        | "mcp_list_prompts"
174        | "mcp_get_prompt"
175        | "mcp_server_info"
176        | "mcp_disconnect" => Some(BuiltinEffectGroup::Mcp),
177        "host_call" | "host_tool_call" | "host_tool_list" => Some(BuiltinEffectGroup::Host),
178        "connector_call" => Some(BuiltinEffectGroup::ConnectorCall),
179        _ => None,
180    }
181}