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    /// Maximum `VmValue` nesting depth accepted by serializers that hand the
55    /// value tree to a third-party recursive encoder we cannot make
56    /// stack-safe internally (pretty JSON, YAML). Our own walks grow the
57    /// native stack on demand (see `value::recursion`); this ceiling keeps
58    /// adversarially nested data from overflowing those external encoders.
59    pub max_value_depth: usize,
60}
61
62impl RuntimeLimits {
63    /// Default profile preserving the legacy hard-coded ceilings.
64    pub const DEFAULT: Self = Self {
65        max_vm_frames: 512,
66        max_template_include_depth: 32,
67        max_template_parse_cache_entries: 128,
68        max_file_text_cache_entries: 256,
69        max_json_parse_cache_entries: 128,
70        max_std_cache_entries: 256,
71        max_regex_cache_entries: 128,
72        max_schema_pattern_cache_entries: 256,
73        max_schema_guard_cache_entries: 256,
74        default_event_log_queue_depth: 128,
75        max_agent_sessions: 128,
76        max_project_fingerprint_depth: 4,
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        max_value_depth: 1024,
86    };
87
88    /// Return the value for a named limit.
89    pub fn value(&self, name: &str) -> Option<usize> {
90        Some(match name {
91            "max_vm_frames" => self.max_vm_frames,
92            "max_template_include_depth" => self.max_template_include_depth,
93            "max_template_parse_cache_entries" => self.max_template_parse_cache_entries,
94            "max_file_text_cache_entries" => self.max_file_text_cache_entries,
95            "max_json_parse_cache_entries" => self.max_json_parse_cache_entries,
96            "max_std_cache_entries" => self.max_std_cache_entries,
97            "max_regex_cache_entries" => self.max_regex_cache_entries,
98            "max_schema_pattern_cache_entries" => self.max_schema_pattern_cache_entries,
99            "max_schema_guard_cache_entries" => self.max_schema_guard_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_project_enrich_yaml_depth" => self.max_project_enrich_yaml_depth,
104            "max_template_ast_depth" => self.max_template_ast_depth,
105            "max_constant_folded_collection_items" => self.max_constant_folded_collection_items,
106            "max_constant_folded_string_bytes" => self.max_constant_folded_string_bytes,
107            "max_nested_execution_depth" => self.max_nested_execution_depth,
108            "max_schema_nudge_depth" => self.max_schema_nudge_depth,
109            "max_schema_nudge_lines" => self.max_schema_nudge_lines,
110            "max_schema_nudge_keys" => self.max_schema_nudge_keys,
111            "max_value_depth" => self.max_value_depth,
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: "default_event_log_queue_depth",
223        user_visible: true,
224        host_configurable: true,
225        protects: "bounds queued subscriber notifications for memory, file, and sqlite event logs",
226    },
227    RuntimeLimitDescription {
228        name: "max_agent_sessions",
229        user_visible: true,
230        host_configurable: false,
231        protects: "bounds concurrent first-class agent sessions stored on one VM thread",
232    },
233    RuntimeLimitDescription {
234        name: "max_project_fingerprint_depth",
235        user_visible: false,
236        host_configurable: false,
237        protects: "bounds project fingerprint discovery in large or adversarial directory trees",
238    },
239    RuntimeLimitDescription {
240        name: "max_project_enrich_yaml_depth",
241        user_visible: false,
242        host_configurable: false,
243        protects: "bounds project enrichment traversal of nested hook YAML",
244    },
245    RuntimeLimitDescription {
246        name: "max_template_ast_depth",
247        user_visible: true,
248        host_configurable: false,
249        protects: "bounds nested prompt-template control structures and expressions",
250    },
251    RuntimeLimitDescription {
252        name: "max_constant_folded_collection_items",
253        user_visible: false,
254        host_configurable: false,
255        protects: "prevents compile-time constant folding from materializing huge collections",
256    },
257    RuntimeLimitDescription {
258        name: "max_constant_folded_string_bytes",
259        user_visible: false,
260        host_configurable: false,
261        protects: "prevents compile-time constant folding from materializing huge strings",
262    },
263    RuntimeLimitDescription {
264        name: "max_nested_execution_depth",
265        user_visible: true,
266        host_configurable: false,
267        protects: "bounds nested agent loops, sub-agents, workers, and workflow stages",
268    },
269    RuntimeLimitDescription {
270        name: "max_schema_nudge_depth",
271        user_visible: false,
272        host_configurable: false,
273        protects: "keeps schema-retry correction prompts compact for deeply nested schemas",
274    },
275    RuntimeLimitDescription {
276        name: "max_schema_nudge_lines",
277        user_visible: false,
278        host_configurable: false,
279        protects: "keeps schema-retry correction prompts from growing with wide schemas",
280    },
281    RuntimeLimitDescription {
282        name: "max_schema_nudge_keys",
283        user_visible: false,
284        host_configurable: false,
285        protects: "keeps schema-retry object-key previews compact for wide objects",
286    },
287    RuntimeLimitDescription {
288        name: "max_value_depth",
289        user_visible: false,
290        host_configurable: false,
291        protects: "bounds value nesting handed to external pretty-JSON/YAML encoders so deep data cannot overflow their recursion",
292    },
293];
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn default_runtime_limits_match_legacy_values() {
301        let limits = RuntimeLimits::default();
302        assert_eq!(limits.max_vm_frames, 512);
303        assert_eq!(limits.max_template_include_depth, 32);
304        assert_eq!(limits.max_template_parse_cache_entries, 128);
305        assert_eq!(limits.max_file_text_cache_entries, 256);
306        assert_eq!(limits.max_json_parse_cache_entries, 128);
307        assert_eq!(limits.max_std_cache_entries, 256);
308        assert_eq!(limits.max_regex_cache_entries, 128);
309        assert_eq!(limits.max_schema_pattern_cache_entries, 256);
310        assert_eq!(limits.max_schema_guard_cache_entries, 256);
311        assert_eq!(limits.default_event_log_queue_depth, 128);
312        assert_eq!(limits.max_agent_sessions, 128);
313        assert_eq!(limits.max_project_fingerprint_depth, 4);
314        assert_eq!(limits.max_project_enrich_yaml_depth, 128);
315        assert_eq!(limits.max_template_ast_depth, 128);
316        assert_eq!(limits.max_constant_folded_collection_items, 4_096);
317        assert_eq!(limits.max_constant_folded_string_bytes, 64 * 1024);
318        assert_eq!(limits.max_nested_execution_depth, 8);
319        assert_eq!(limits.max_schema_nudge_depth, 3);
320        assert_eq!(limits.max_schema_nudge_lines, 8);
321        assert_eq!(limits.max_schema_nudge_keys, 16);
322        assert_eq!(limits.max_value_depth, 1024);
323    }
324
325    #[test]
326    fn runtime_limit_report_documents_every_field() {
327        let report = RuntimeLimits::default().report();
328        let names = report
329            .entries
330            .iter()
331            .map(|entry| entry.name)
332            .collect::<Vec<_>>();
333        assert_eq!(
334            names,
335            vec![
336                "max_vm_frames",
337                "max_template_include_depth",
338                "max_template_parse_cache_entries",
339                "max_file_text_cache_entries",
340                "max_json_parse_cache_entries",
341                "max_std_cache_entries",
342                "max_regex_cache_entries",
343                "max_schema_pattern_cache_entries",
344                "max_schema_guard_cache_entries",
345                "default_event_log_queue_depth",
346                "max_agent_sessions",
347                "max_project_fingerprint_depth",
348                "max_project_enrich_yaml_depth",
349                "max_template_ast_depth",
350                "max_constant_folded_collection_items",
351                "max_constant_folded_string_bytes",
352                "max_nested_execution_depth",
353                "max_schema_nudge_depth",
354                "max_schema_nudge_lines",
355                "max_schema_nudge_keys",
356                "max_value_depth",
357            ]
358        );
359        assert!(report.entries.iter().all(|entry| entry.value > 0));
360        assert!(report
361            .entries
362            .iter()
363            .all(|entry| !entry.protects.is_empty()));
364    }
365}