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