harn_vm/connectors/
effect_policy.rs1use 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_download"
157 | "http_request"
158 | "http_session_request"
159 | "http_stream_open"
160 | "http_stream_read"
161 | "http_stream_close"
162 | "http_stream_info"
163 | "sse_connect"
164 | "sse_receive"
165 | "websocket_accept"
166 | "websocket_connect"
167 | "websocket_route"
168 | "websocket_send"
169 | "websocket_receive"
170 | "websocket_server" => Some(BuiltinEffectGroup::Network),
171 "llm_call" | "llm_call_safe" | "llm_completion" | "llm_stream" | "llm_healthcheck"
172 | "agent_loop" => Some(BuiltinEffectGroup::Llm),
173 "vision_ocr" => Some(BuiltinEffectGroup::Process),
174 "mcp_connect"
175 | "mcp_ensure_active"
176 | "mcp_call"
177 | "mcp_list_tools"
178 | "mcp_list_resources"
179 | "mcp_list_resource_templates"
180 | "mcp_read_resource"
181 | "mcp_list_prompts"
182 | "mcp_get_prompt"
183 | "mcp_server_info"
184 | "mcp_disconnect" => Some(BuiltinEffectGroup::Mcp),
185 "host_call" | "host_tool_call" | "host_tool_list" => Some(BuiltinEffectGroup::Host),
186 "connector_call" => Some(BuiltinEffectGroup::ConnectorCall),
187 _ => None,
188 }
189}