Skip to main content

harn_vm/
runtime_limits.rs

1//! Central runtime ceilings for VM execution and stdlib resource guards.
2//!
3//! These limits are intentionally concrete fields instead of a stringly-typed
4//! config map. Most are internal guardrails for adversarial recursion, memory
5//! growth, or prompt/debug helper expansion. Hosts can report the effective
6//! profile through [`RuntimeLimits::report`], but these defaults are not a
7//! broad user-facing configuration surface.
8
9use serde::Serialize;
10
11/// Fixed resource and recursion ceilings used by the VM/runtime layer.
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
13pub struct RuntimeLimits {
14    /// Maximum nested Harn call frames before the VM raises stack overflow.
15    pub max_vm_frames: usize,
16    /// Maximum nested `.harn.prompt` include depth.
17    pub max_template_include_depth: usize,
18    /// Maximum parsed template assets kept in the per-thread template cache.
19    pub max_template_parse_cache_entries: usize,
20    /// Maximum UTF-8 file reads kept in the per-thread stdlib file cache.
21    pub max_file_text_cache_entries: usize,
22    /// Maximum memoized `json_parse` inputs kept per VM thread.
23    pub max_json_parse_cache_entries: usize,
24    /// Default maximum entries for the persistent `std/cache` backends.
25    pub max_std_cache_entries: usize,
26    /// Maximum compiled regex entries kept per VM thread.
27    pub max_regex_cache_entries: usize,
28    /// Maximum compiled JSON-schema `pattern` regexes kept process-wide.
29    pub max_schema_pattern_cache_entries: usize,
30    /// Maximum canonical parameter schemas kept per VM thread.
31    pub max_schema_guard_cache_entries: usize,
32    /// Default queue depth for memory/file/sqlite event-log subscribers.
33    pub default_event_log_queue_depth: usize,
34    /// Default concurrent agent-session cap per VM thread.
35    pub max_agent_sessions: usize,
36    /// Maximum directory descent for project fingerprint discovery.
37    pub max_project_fingerprint_depth: usize,
38    /// Maximum nested YAML depth walked while extracting project enrichment hints.
39    pub max_project_enrich_yaml_depth: usize,
40    /// Maximum nested prompt-template AST/expression depth.
41    pub max_template_ast_depth: usize,
42    /// Maximum collection items the compiler may materialize while folding constants.
43    pub max_constant_folded_collection_items: usize,
44    /// Maximum string bytes the compiler may materialize while folding constants.
45    pub max_constant_folded_string_bytes: usize,
46    /// Default Harn-side nested-execution budget installed by the builtin policy ceiling.
47    pub max_nested_execution_depth: usize,
48    /// Maximum schema nesting shown in LLM schema-correction nudges.
49    pub max_schema_nudge_depth: usize,
50    /// Maximum schema lines shown in LLM schema-correction nudges.
51    pub max_schema_nudge_lines: usize,
52    /// Maximum object keys listed in LLM schema-correction nudges.
53    pub max_schema_nudge_keys: usize,
54}
55
56impl RuntimeLimits {
57    /// Default profile preserving the legacy hard-coded ceilings.
58    pub const DEFAULT: Self = Self {
59        max_vm_frames: 512,
60        max_template_include_depth: 32,
61        max_template_parse_cache_entries: 128,
62        max_file_text_cache_entries: 256,
63        max_json_parse_cache_entries: 128,
64        max_std_cache_entries: 256,
65        max_regex_cache_entries: 128,
66        max_schema_pattern_cache_entries: 256,
67        max_schema_guard_cache_entries: 256,
68        default_event_log_queue_depth: 128,
69        max_agent_sessions: 128,
70        max_project_fingerprint_depth: 4,
71        max_project_enrich_yaml_depth: 128,
72        max_template_ast_depth: 128,
73        max_constant_folded_collection_items: 4_096,
74        max_constant_folded_string_bytes: 64 * 1024,
75        max_nested_execution_depth: 8,
76        max_schema_nudge_depth: 3,
77        max_schema_nudge_lines: 8,
78        max_schema_nudge_keys: 16,
79    };
80
81    /// Return the value for a named limit.
82    pub fn value(&self, name: &str) -> Option<usize> {
83        Some(match name {
84            "max_vm_frames" => self.max_vm_frames,
85            "max_template_include_depth" => self.max_template_include_depth,
86            "max_template_parse_cache_entries" => self.max_template_parse_cache_entries,
87            "max_file_text_cache_entries" => self.max_file_text_cache_entries,
88            "max_json_parse_cache_entries" => self.max_json_parse_cache_entries,
89            "max_std_cache_entries" => self.max_std_cache_entries,
90            "max_regex_cache_entries" => self.max_regex_cache_entries,
91            "max_schema_pattern_cache_entries" => self.max_schema_pattern_cache_entries,
92            "max_schema_guard_cache_entries" => self.max_schema_guard_cache_entries,
93            "default_event_log_queue_depth" => self.default_event_log_queue_depth,
94            "max_agent_sessions" => self.max_agent_sessions,
95            "max_project_fingerprint_depth" => self.max_project_fingerprint_depth,
96            "max_project_enrich_yaml_depth" => self.max_project_enrich_yaml_depth,
97            "max_template_ast_depth" => self.max_template_ast_depth,
98            "max_constant_folded_collection_items" => self.max_constant_folded_collection_items,
99            "max_constant_folded_string_bytes" => self.max_constant_folded_string_bytes,
100            "max_nested_execution_depth" => self.max_nested_execution_depth,
101            "max_schema_nudge_depth" => self.max_schema_nudge_depth,
102            "max_schema_nudge_lines" => self.max_schema_nudge_lines,
103            "max_schema_nudge_keys" => self.max_schema_nudge_keys,
104            _ => return None,
105        })
106    }
107
108    /// Build a host-readable report for the effective limit profile.
109    pub fn report(&self) -> RuntimeLimitsReport {
110        RuntimeLimitsReport {
111            entries: RUNTIME_LIMIT_DESCRIPTIONS
112                .iter()
113                .map(|description| RuntimeLimitEntry {
114                    name: description.name,
115                    value: self
116                        .value(description.name)
117                        .expect("runtime limit description must name a RuntimeLimits field"),
118                    user_visible: description.user_visible,
119                    host_configurable: description.host_configurable,
120                    protects: description.protects,
121                })
122                .collect(),
123        }
124    }
125}
126
127impl Default for RuntimeLimits {
128    fn default() -> Self {
129        Self::DEFAULT
130    }
131}
132
133/// Host/debug report for an effective runtime-limits profile.
134#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
135pub struct RuntimeLimitsReport {
136    pub entries: Vec<RuntimeLimitEntry>,
137}
138
139/// One documented runtime limit with its effective value.
140#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
141pub struct RuntimeLimitEntry {
142    pub name: &'static str,
143    pub value: usize,
144    pub user_visible: bool,
145    pub host_configurable: bool,
146    pub protects: &'static str,
147}
148
149/// Static documentation for every runtime limit field.
150#[derive(Clone, Copy, Debug, Eq, PartialEq)]
151pub struct RuntimeLimitDescription {
152    pub name: &'static str,
153    pub user_visible: bool,
154    pub host_configurable: bool,
155    pub protects: &'static str,
156}
157
158pub const RUNTIME_LIMIT_DESCRIPTIONS: &[RuntimeLimitDescription] = &[
159    RuntimeLimitDescription {
160        name: "max_vm_frames",
161        user_visible: true,
162        host_configurable: false,
163        protects: "prevents unbounded Harn function recursion from exhausting the VM stack",
164    },
165    RuntimeLimitDescription {
166        name: "max_template_include_depth",
167        user_visible: true,
168        host_configurable: false,
169        protects: "bounds recursive prompt-template includes after cycle detection",
170    },
171    RuntimeLimitDescription {
172        name: "max_template_parse_cache_entries",
173        user_visible: false,
174        host_configurable: false,
175        protects: "bounds per-thread memory held by parsed prompt-template assets",
176    },
177    RuntimeLimitDescription {
178        name: "max_file_text_cache_entries",
179        user_visible: false,
180        host_configurable: false,
181        protects: "bounds per-thread memory held by cached UTF-8 file reads",
182    },
183    RuntimeLimitDescription {
184        name: "max_json_parse_cache_entries",
185        user_visible: false,
186        host_configurable: false,
187        protects: "bounds per-thread memory held by memoized JSON parse inputs and values",
188    },
189    RuntimeLimitDescription {
190        name: "max_std_cache_entries",
191        user_visible: true,
192        host_configurable: false,
193        protects: "bounds retained entries in std/cache memory, filesystem, and sqlite backends",
194    },
195    RuntimeLimitDescription {
196        name: "max_regex_cache_entries",
197        user_visible: false,
198        host_configurable: false,
199        protects: "bounds per-thread memory held by compiled stdlib regex patterns",
200    },
201    RuntimeLimitDescription {
202        name: "max_schema_pattern_cache_entries",
203        user_visible: false,
204        host_configurable: false,
205        protects: "bounds process-wide memory held by compiled JSON-schema pattern regexes",
206    },
207    RuntimeLimitDescription {
208        name: "max_schema_guard_cache_entries",
209        user_visible: false,
210        host_configurable: false,
211        protects: "bounds per-thread memory held by canonical runtime parameter schemas",
212    },
213    RuntimeLimitDescription {
214        name: "default_event_log_queue_depth",
215        user_visible: true,
216        host_configurable: true,
217        protects: "bounds queued subscriber notifications for memory, file, and sqlite event logs",
218    },
219    RuntimeLimitDescription {
220        name: "max_agent_sessions",
221        user_visible: true,
222        host_configurable: false,
223        protects: "bounds concurrent first-class agent sessions stored on one VM thread",
224    },
225    RuntimeLimitDescription {
226        name: "max_project_fingerprint_depth",
227        user_visible: false,
228        host_configurable: false,
229        protects: "bounds project fingerprint discovery in large or adversarial directory trees",
230    },
231    RuntimeLimitDescription {
232        name: "max_project_enrich_yaml_depth",
233        user_visible: false,
234        host_configurable: false,
235        protects: "bounds project enrichment traversal of nested hook YAML",
236    },
237    RuntimeLimitDescription {
238        name: "max_template_ast_depth",
239        user_visible: true,
240        host_configurable: false,
241        protects: "bounds nested prompt-template control structures and expressions",
242    },
243    RuntimeLimitDescription {
244        name: "max_constant_folded_collection_items",
245        user_visible: false,
246        host_configurable: false,
247        protects: "prevents compile-time constant folding from materializing huge collections",
248    },
249    RuntimeLimitDescription {
250        name: "max_constant_folded_string_bytes",
251        user_visible: false,
252        host_configurable: false,
253        protects: "prevents compile-time constant folding from materializing huge strings",
254    },
255    RuntimeLimitDescription {
256        name: "max_nested_execution_depth",
257        user_visible: true,
258        host_configurable: false,
259        protects: "bounds nested agent loops, sub-agents, workers, and workflow stages",
260    },
261    RuntimeLimitDescription {
262        name: "max_schema_nudge_depth",
263        user_visible: false,
264        host_configurable: false,
265        protects: "keeps schema-retry correction prompts compact for deeply nested schemas",
266    },
267    RuntimeLimitDescription {
268        name: "max_schema_nudge_lines",
269        user_visible: false,
270        host_configurable: false,
271        protects: "keeps schema-retry correction prompts from growing with wide schemas",
272    },
273    RuntimeLimitDescription {
274        name: "max_schema_nudge_keys",
275        user_visible: false,
276        host_configurable: false,
277        protects: "keeps schema-retry object-key previews compact for wide objects",
278    },
279];
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn default_runtime_limits_match_legacy_values() {
287        let limits = RuntimeLimits::default();
288        assert_eq!(limits.max_vm_frames, 512);
289        assert_eq!(limits.max_template_include_depth, 32);
290        assert_eq!(limits.max_template_parse_cache_entries, 128);
291        assert_eq!(limits.max_file_text_cache_entries, 256);
292        assert_eq!(limits.max_json_parse_cache_entries, 128);
293        assert_eq!(limits.max_std_cache_entries, 256);
294        assert_eq!(limits.max_regex_cache_entries, 128);
295        assert_eq!(limits.max_schema_pattern_cache_entries, 256);
296        assert_eq!(limits.max_schema_guard_cache_entries, 256);
297        assert_eq!(limits.default_event_log_queue_depth, 128);
298        assert_eq!(limits.max_agent_sessions, 128);
299        assert_eq!(limits.max_project_fingerprint_depth, 4);
300        assert_eq!(limits.max_project_enrich_yaml_depth, 128);
301        assert_eq!(limits.max_template_ast_depth, 128);
302        assert_eq!(limits.max_constant_folded_collection_items, 4_096);
303        assert_eq!(limits.max_constant_folded_string_bytes, 64 * 1024);
304        assert_eq!(limits.max_nested_execution_depth, 8);
305        assert_eq!(limits.max_schema_nudge_depth, 3);
306        assert_eq!(limits.max_schema_nudge_lines, 8);
307        assert_eq!(limits.max_schema_nudge_keys, 16);
308    }
309
310    #[test]
311    fn runtime_limit_report_documents_every_field() {
312        let report = RuntimeLimits::default().report();
313        let names = report
314            .entries
315            .iter()
316            .map(|entry| entry.name)
317            .collect::<Vec<_>>();
318        assert_eq!(
319            names,
320            vec![
321                "max_vm_frames",
322                "max_template_include_depth",
323                "max_template_parse_cache_entries",
324                "max_file_text_cache_entries",
325                "max_json_parse_cache_entries",
326                "max_std_cache_entries",
327                "max_regex_cache_entries",
328                "max_schema_pattern_cache_entries",
329                "max_schema_guard_cache_entries",
330                "default_event_log_queue_depth",
331                "max_agent_sessions",
332                "max_project_fingerprint_depth",
333                "max_project_enrich_yaml_depth",
334                "max_template_ast_depth",
335                "max_constant_folded_collection_items",
336                "max_constant_folded_string_bytes",
337                "max_nested_execution_depth",
338                "max_schema_nudge_depth",
339                "max_schema_nudge_lines",
340                "max_schema_nudge_keys",
341            ]
342        );
343        assert!(report.entries.iter().all(|entry| entry.value > 0));
344        assert!(report
345            .entries
346            .iter()
347            .all(|entry| !entry.protects.is_empty()));
348    }
349}