Skip to main content

harn_hostlib/
schemas.rs

1//! Embedded JSON Schemas for every hostlib host method.
2//!
3//! Schemas live at `schemas/<module>/<method>.{request,response}.json` and
4//! are baked into the crate at compile time via `include_str!`. They're the
5//! source of truth for hostlib request/response compatibility: the schema
6//! files ship with the crate (see the `include` field in `Cargo.toml`),
7//! and consumers fetch them through this module.
8//!
9//! Schemas use JSON Schema draft 2020-12.
10
11use std::sync::OnceLock;
12
13use harn_vm::{VmDictExt, VmValue};
14
15use crate::error::HostlibError;
16
17/// Direction of a schema (request body vs. response body).
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum SchemaKind {
20    /// Schema for the *input* of a host method.
21    Request,
22    /// Schema for the *output* of a host method.
23    Response,
24}
25
26/// One `(module, method, kind, schema_text)` tuple for every shipped schema.
27///
28/// Embedders use this catalog to:
29/// - assert that every registered builtin has a matching schema (drift test);
30/// - export the schemas to downstream consumers;
31/// - validate live request/response payloads in tests.
32pub const SCHEMAS: &[(&str, &str, SchemaKind, &str)] = &[
33    // ast/
34    (
35        "ast",
36        "parse_file",
37        SchemaKind::Request,
38        include_str!("../schemas/ast/parse_file.request.json"),
39    ),
40    (
41        "ast",
42        "parse_file",
43        SchemaKind::Response,
44        include_str!("../schemas/ast/parse_file.response.json"),
45    ),
46    (
47        "ast",
48        "symbols",
49        SchemaKind::Request,
50        include_str!("../schemas/ast/symbols.request.json"),
51    ),
52    (
53        "ast",
54        "symbols",
55        SchemaKind::Response,
56        include_str!("../schemas/ast/symbols.response.json"),
57    ),
58    (
59        "ast",
60        "outline",
61        SchemaKind::Request,
62        include_str!("../schemas/ast/outline.request.json"),
63    ),
64    (
65        "ast",
66        "outline",
67        SchemaKind::Response,
68        include_str!("../schemas/ast/outline.response.json"),
69    ),
70    (
71        "ast",
72        "parse_errors",
73        SchemaKind::Request,
74        include_str!("../schemas/ast/parse_errors.request.json"),
75    ),
76    (
77        "ast",
78        "parse_errors",
79        SchemaKind::Response,
80        include_str!("../schemas/ast/parse_errors.response.json"),
81    ),
82    (
83        "ast",
84        "undefined_names",
85        SchemaKind::Request,
86        include_str!("../schemas/ast/undefined_names.request.json"),
87    ),
88    (
89        "ast",
90        "undefined_names",
91        SchemaKind::Response,
92        include_str!("../schemas/ast/undefined_names.response.json"),
93    ),
94    (
95        "ast",
96        "function_body",
97        SchemaKind::Request,
98        include_str!("../schemas/ast/function_body.request.json"),
99    ),
100    (
101        "ast",
102        "function_body",
103        SchemaKind::Response,
104        include_str!("../schemas/ast/function_body.response.json"),
105    ),
106    (
107        "ast",
108        "function_bodies",
109        SchemaKind::Request,
110        include_str!("../schemas/ast/function_bodies.request.json"),
111    ),
112    (
113        "ast",
114        "function_bodies",
115        SchemaKind::Response,
116        include_str!("../schemas/ast/function_bodies.response.json"),
117    ),
118    (
119        "ast",
120        "extract_imports",
121        SchemaKind::Request,
122        include_str!("../schemas/ast/extract_imports.request.json"),
123    ),
124    (
125        "ast",
126        "extract_imports",
127        SchemaKind::Response,
128        include_str!("../schemas/ast/extract_imports.response.json"),
129    ),
130    (
131        "ast",
132        "symbol_extract",
133        SchemaKind::Request,
134        include_str!("../schemas/ast/symbol_extract.request.json"),
135    ),
136    (
137        "ast",
138        "symbol_extract",
139        SchemaKind::Response,
140        include_str!("../schemas/ast/symbol_extract.response.json"),
141    ),
142    (
143        "ast",
144        "symbol_delete",
145        SchemaKind::Request,
146        include_str!("../schemas/ast/symbol_delete.request.json"),
147    ),
148    (
149        "ast",
150        "symbol_delete",
151        SchemaKind::Response,
152        include_str!("../schemas/ast/symbol_delete.response.json"),
153    ),
154    (
155        "ast",
156        "symbol_replace",
157        SchemaKind::Request,
158        include_str!("../schemas/ast/symbol_replace.request.json"),
159    ),
160    (
161        "ast",
162        "symbol_replace",
163        SchemaKind::Response,
164        include_str!("../schemas/ast/symbol_replace.response.json"),
165    ),
166    (
167        "ast",
168        "bracket_balance",
169        SchemaKind::Request,
170        include_str!("../schemas/ast/bracket_balance.request.json"),
171    ),
172    (
173        "ast",
174        "bracket_balance",
175        SchemaKind::Response,
176        include_str!("../schemas/ast/bracket_balance.response.json"),
177    ),
178    (
179        "ast",
180        "apply_node",
181        SchemaKind::Request,
182        include_str!("../schemas/ast/apply_node.request.json"),
183    ),
184    (
185        "ast",
186        "apply_node",
187        SchemaKind::Response,
188        include_str!("../schemas/ast/apply_node.response.json"),
189    ),
190    (
191        "ast",
192        "insert_at_anchor",
193        SchemaKind::Request,
194        include_str!("../schemas/ast/insert_at_anchor.request.json"),
195    ),
196    (
197        "ast",
198        "insert_at_anchor",
199        SchemaKind::Response,
200        include_str!("../schemas/ast/insert_at_anchor.response.json"),
201    ),
202    (
203        "ast",
204        "batch_apply",
205        SchemaKind::Request,
206        include_str!("../schemas/ast/batch_apply.request.json"),
207    ),
208    (
209        "ast",
210        "batch_apply",
211        SchemaKind::Response,
212        include_str!("../schemas/ast/batch_apply.response.json"),
213    ),
214    (
215        "ast",
216        "dry_run",
217        SchemaKind::Request,
218        include_str!("../schemas/ast/dry_run.request.json"),
219    ),
220    (
221        "ast",
222        "dry_run",
223        SchemaKind::Response,
224        include_str!("../schemas/ast/dry_run.response.json"),
225    ),
226    (
227        "ast",
228        "search",
229        SchemaKind::Request,
230        include_str!("../schemas/ast/search.request.json"),
231    ),
232    (
233        "ast",
234        "search",
235        SchemaKind::Response,
236        include_str!("../schemas/ast/search.response.json"),
237    ),
238    (
239        "ast",
240        "structural_diff",
241        SchemaKind::Request,
242        include_str!("../schemas/ast/structural_diff.request.json"),
243    ),
244    (
245        "ast",
246        "structural_diff",
247        SchemaKind::Response,
248        include_str!("../schemas/ast/structural_diff.response.json"),
249    ),
250    (
251        "ast",
252        "capabilities",
253        SchemaKind::Request,
254        include_str!("../schemas/ast/capabilities.request.json"),
255    ),
256    (
257        "ast",
258        "capabilities",
259        SchemaKind::Response,
260        include_str!("../schemas/ast/capabilities.response.json"),
261    ),
262    // code_index/
263    (
264        "code_index",
265        "query",
266        SchemaKind::Request,
267        include_str!("../schemas/code_index/query.request.json"),
268    ),
269    (
270        "code_index",
271        "query",
272        SchemaKind::Response,
273        include_str!("../schemas/code_index/query.response.json"),
274    ),
275    (
276        "code_index",
277        "rebuild",
278        SchemaKind::Request,
279        include_str!("../schemas/code_index/rebuild.request.json"),
280    ),
281    (
282        "code_index",
283        "rebuild",
284        SchemaKind::Response,
285        include_str!("../schemas/code_index/rebuild.response.json"),
286    ),
287    (
288        "code_index",
289        "stats",
290        SchemaKind::Request,
291        include_str!("../schemas/code_index/stats.request.json"),
292    ),
293    (
294        "code_index",
295        "stats",
296        SchemaKind::Response,
297        include_str!("../schemas/code_index/stats.response.json"),
298    ),
299    (
300        "code_index",
301        "imports_for",
302        SchemaKind::Request,
303        include_str!("../schemas/code_index/imports_for.request.json"),
304    ),
305    (
306        "code_index",
307        "imports_for",
308        SchemaKind::Response,
309        include_str!("../schemas/code_index/imports_for.response.json"),
310    ),
311    (
312        "code_index",
313        "importers_of",
314        SchemaKind::Request,
315        include_str!("../schemas/code_index/importers_of.request.json"),
316    ),
317    (
318        "code_index",
319        "importers_of",
320        SchemaKind::Response,
321        include_str!("../schemas/code_index/importers_of.response.json"),
322    ),
323    // code_index — additive read-only secondary roots (#2403 follow-up)
324    (
325        "code_index",
326        "add_readonly_roots",
327        SchemaKind::Request,
328        include_str!("../schemas/code_index/add_readonly_roots.request.json"),
329    ),
330    (
331        "code_index",
332        "add_readonly_roots",
333        SchemaKind::Response,
334        include_str!("../schemas/code_index/add_readonly_roots.response.json"),
335    ),
336    // code_index — file table accessors
337    (
338        "code_index",
339        "path_to_id",
340        SchemaKind::Request,
341        include_str!("../schemas/code_index/path_to_id.request.json"),
342    ),
343    (
344        "code_index",
345        "path_to_id",
346        SchemaKind::Response,
347        include_str!("../schemas/code_index/path_to_id.response.json"),
348    ),
349    (
350        "code_index",
351        "id_to_path",
352        SchemaKind::Request,
353        include_str!("../schemas/code_index/id_to_path.request.json"),
354    ),
355    (
356        "code_index",
357        "id_to_path",
358        SchemaKind::Response,
359        include_str!("../schemas/code_index/id_to_path.response.json"),
360    ),
361    (
362        "code_index",
363        "file_ids",
364        SchemaKind::Request,
365        include_str!("../schemas/code_index/file_ids.request.json"),
366    ),
367    (
368        "code_index",
369        "file_ids",
370        SchemaKind::Response,
371        include_str!("../schemas/code_index/file_ids.response.json"),
372    ),
373    (
374        "code_index",
375        "file_meta",
376        SchemaKind::Request,
377        include_str!("../schemas/code_index/file_meta.request.json"),
378    ),
379    (
380        "code_index",
381        "file_meta",
382        SchemaKind::Response,
383        include_str!("../schemas/code_index/file_meta.response.json"),
384    ),
385    (
386        "code_index",
387        "file_hash",
388        SchemaKind::Request,
389        include_str!("../schemas/code_index/file_hash.request.json"),
390    ),
391    (
392        "code_index",
393        "file_hash",
394        SchemaKind::Response,
395        include_str!("../schemas/code_index/file_hash.response.json"),
396    ),
397    (
398        "code_index",
399        "file_hash_snapshot",
400        SchemaKind::Request,
401        include_str!("../schemas/code_index/file_hash_snapshot.request.json"),
402    ),
403    (
404        "code_index",
405        "file_hash_snapshot",
406        SchemaKind::Response,
407        include_str!("../schemas/code_index/file_hash_snapshot.response.json"),
408    ),
409    // code_index — cached reads
410    (
411        "code_index",
412        "read_range",
413        SchemaKind::Request,
414        include_str!("../schemas/code_index/read_range.request.json"),
415    ),
416    (
417        "code_index",
418        "read_range",
419        SchemaKind::Response,
420        include_str!("../schemas/code_index/read_range.response.json"),
421    ),
422    (
423        "code_index",
424        "reindex_file",
425        SchemaKind::Request,
426        include_str!("../schemas/code_index/reindex_file.request.json"),
427    ),
428    (
429        "code_index",
430        "reindex_file",
431        SchemaKind::Response,
432        include_str!("../schemas/code_index/reindex_file.response.json"),
433    ),
434    (
435        "code_index",
436        "trigram_query",
437        SchemaKind::Request,
438        include_str!("../schemas/code_index/trigram_query.request.json"),
439    ),
440    (
441        "code_index",
442        "trigram_query",
443        SchemaKind::Response,
444        include_str!("../schemas/code_index/trigram_query.response.json"),
445    ),
446    (
447        "code_index",
448        "extract_trigrams",
449        SchemaKind::Request,
450        include_str!("../schemas/code_index/extract_trigrams.request.json"),
451    ),
452    (
453        "code_index",
454        "extract_trigrams",
455        SchemaKind::Response,
456        include_str!("../schemas/code_index/extract_trigrams.response.json"),
457    ),
458    (
459        "code_index",
460        "word_get",
461        SchemaKind::Request,
462        include_str!("../schemas/code_index/word_get.request.json"),
463    ),
464    (
465        "code_index",
466        "word_get",
467        SchemaKind::Response,
468        include_str!("../schemas/code_index/word_get.response.json"),
469    ),
470    (
471        "code_index",
472        "deps_get",
473        SchemaKind::Request,
474        include_str!("../schemas/code_index/deps_get.request.json"),
475    ),
476    (
477        "code_index",
478        "deps_get",
479        SchemaKind::Response,
480        include_str!("../schemas/code_index/deps_get.response.json"),
481    ),
482    (
483        "code_index",
484        "outline_get",
485        SchemaKind::Request,
486        include_str!("../schemas/code_index/outline_get.request.json"),
487    ),
488    (
489        "code_index",
490        "outline_get",
491        SchemaKind::Response,
492        include_str!("../schemas/code_index/outline_get.response.json"),
493    ),
494    // code_index — change log
495    (
496        "code_index",
497        "current_seq",
498        SchemaKind::Request,
499        include_str!("../schemas/code_index/current_seq.request.json"),
500    ),
501    (
502        "code_index",
503        "current_seq",
504        SchemaKind::Response,
505        include_str!("../schemas/code_index/current_seq.response.json"),
506    ),
507    (
508        "code_index",
509        "changes_since",
510        SchemaKind::Request,
511        include_str!("../schemas/code_index/changes_since.request.json"),
512    ),
513    (
514        "code_index",
515        "changes_since",
516        SchemaKind::Response,
517        include_str!("../schemas/code_index/changes_since.response.json"),
518    ),
519    (
520        "code_index",
521        "version_record",
522        SchemaKind::Request,
523        include_str!("../schemas/code_index/version_record.request.json"),
524    ),
525    (
526        "code_index",
527        "version_record",
528        SchemaKind::Response,
529        include_str!("../schemas/code_index/version_record.response.json"),
530    ),
531    // code_index — agents + locks
532    (
533        "code_index",
534        "agent_register",
535        SchemaKind::Request,
536        include_str!("../schemas/code_index/agent_register.request.json"),
537    ),
538    (
539        "code_index",
540        "agent_register",
541        SchemaKind::Response,
542        include_str!("../schemas/code_index/agent_register.response.json"),
543    ),
544    (
545        "code_index",
546        "agent_heartbeat",
547        SchemaKind::Request,
548        include_str!("../schemas/code_index/agent_heartbeat.request.json"),
549    ),
550    (
551        "code_index",
552        "agent_heartbeat",
553        SchemaKind::Response,
554        include_str!("../schemas/code_index/agent_heartbeat.response.json"),
555    ),
556    (
557        "code_index",
558        "agent_unregister",
559        SchemaKind::Request,
560        include_str!("../schemas/code_index/agent_unregister.request.json"),
561    ),
562    (
563        "code_index",
564        "agent_unregister",
565        SchemaKind::Response,
566        include_str!("../schemas/code_index/agent_unregister.response.json"),
567    ),
568    (
569        "code_index",
570        "lock_try",
571        SchemaKind::Request,
572        include_str!("../schemas/code_index/lock_try.request.json"),
573    ),
574    (
575        "code_index",
576        "lock_try",
577        SchemaKind::Response,
578        include_str!("../schemas/code_index/lock_try.response.json"),
579    ),
580    (
581        "code_index",
582        "lock_release",
583        SchemaKind::Request,
584        include_str!("../schemas/code_index/lock_release.request.json"),
585    ),
586    (
587        "code_index",
588        "lock_release",
589        SchemaKind::Response,
590        include_str!("../schemas/code_index/lock_release.response.json"),
591    ),
592    (
593        "code_index",
594        "status",
595        SchemaKind::Request,
596        include_str!("../schemas/code_index/status.request.json"),
597    ),
598    (
599        "code_index",
600        "status",
601        SchemaKind::Response,
602        include_str!("../schemas/code_index/status.response.json"),
603    ),
604    (
605        "code_index",
606        "current_agent_id",
607        SchemaKind::Request,
608        include_str!("../schemas/code_index/current_agent_id.request.json"),
609    ),
610    (
611        "code_index",
612        "current_agent_id",
613        SchemaKind::Response,
614        include_str!("../schemas/code_index/current_agent_id.response.json"),
615    ),
616    (
617        "code_index",
618        "cypher",
619        SchemaKind::Request,
620        include_str!("../schemas/code_index/cypher.request.json"),
621    ),
622    (
623        "code_index",
624        "cypher",
625        SchemaKind::Response,
626        include_str!("../schemas/code_index/cypher.response.json"),
627    ),
628    (
629        "code_index",
630        "repo_map",
631        SchemaKind::Request,
632        include_str!("../schemas/code_index/repo_map.request.json"),
633    ),
634    (
635        "code_index",
636        "repo_map",
637        SchemaKind::Response,
638        include_str!("../schemas/code_index/repo_map.response.json"),
639    ),
640    (
641        "code_index",
642        "branch_overlay",
643        SchemaKind::Request,
644        include_str!("../schemas/code_index/branch_overlay.request.json"),
645    ),
646    (
647        "code_index",
648        "branch_overlay",
649        SchemaKind::Response,
650        include_str!("../schemas/code_index/branch_overlay.response.json"),
651    ),
652    (
653        "code_index",
654        "freshness",
655        SchemaKind::Request,
656        include_str!("../schemas/code_index/freshness.request.json"),
657    ),
658    (
659        "code_index",
660        "freshness",
661        SchemaKind::Response,
662        include_str!("../schemas/code_index/freshness.response.json"),
663    ),
664    (
665        "code_index",
666        "rename_symbol",
667        SchemaKind::Request,
668        include_str!("../schemas/code_index/rename_symbol.request.json"),
669    ),
670    (
671        "code_index",
672        "rename_symbol",
673        SchemaKind::Response,
674        include_str!("../schemas/code_index/rename_symbol.response.json"),
675    ),
676    // scanner/
677    (
678        "scanner",
679        "scan_project",
680        SchemaKind::Request,
681        include_str!("../schemas/scanner/scan_project.request.json"),
682    ),
683    (
684        "scanner",
685        "scan_project",
686        SchemaKind::Response,
687        include_str!("../schemas/scanner/scan_project.response.json"),
688    ),
689    (
690        "scanner",
691        "scan_incremental",
692        SchemaKind::Request,
693        include_str!("../schemas/scanner/scan_incremental.request.json"),
694    ),
695    (
696        "scanner",
697        "scan_incremental",
698        SchemaKind::Response,
699        include_str!("../schemas/scanner/scan_incremental.response.json"),
700    ),
701    // fs/
702    (
703        "fs",
704        "set_mode",
705        SchemaKind::Request,
706        include_str!("../schemas/fs/set_mode.request.json"),
707    ),
708    (
709        "fs",
710        "set_mode",
711        SchemaKind::Response,
712        include_str!("../schemas/fs/set_mode.response.json"),
713    ),
714    (
715        "fs",
716        "staged_status",
717        SchemaKind::Request,
718        include_str!("../schemas/fs/staged_status.request.json"),
719    ),
720    (
721        "fs",
722        "staged_status",
723        SchemaKind::Response,
724        include_str!("../schemas/fs/staged_status.response.json"),
725    ),
726    (
727        "fs",
728        "commit_staged",
729        SchemaKind::Request,
730        include_str!("../schemas/fs/commit_staged.request.json"),
731    ),
732    (
733        "fs",
734        "commit_staged",
735        SchemaKind::Response,
736        include_str!("../schemas/fs/commit_staged.response.json"),
737    ),
738    (
739        "fs",
740        "discard_staged",
741        SchemaKind::Request,
742        include_str!("../schemas/fs/discard_staged.request.json"),
743    ),
744    (
745        "fs",
746        "discard_staged",
747        SchemaKind::Response,
748        include_str!("../schemas/fs/discard_staged.response.json"),
749    ),
750    (
751        "fs",
752        "safe_text_patch",
753        SchemaKind::Request,
754        include_str!("../schemas/fs/safe_text_patch.request.json"),
755    ),
756    (
757        "fs",
758        "safe_text_patch",
759        SchemaKind::Response,
760        include_str!("../schemas/fs/safe_text_patch.response.json"),
761    ),
762    (
763        "fs",
764        "read_text",
765        SchemaKind::Request,
766        include_str!("../schemas/fs/read_text.request.json"),
767    ),
768    (
769        "fs",
770        "read_text",
771        SchemaKind::Response,
772        include_str!("../schemas/fs/read_text.response.json"),
773    ),
774    (
775        "fs",
776        "emit_safe_text_patch_result",
777        SchemaKind::Request,
778        include_str!("../schemas/fs/emit_safe_text_patch_result.request.json"),
779    ),
780    (
781        "fs",
782        "emit_safe_text_patch_result",
783        SchemaKind::Response,
784        include_str!("../schemas/fs/emit_safe_text_patch_result.response.json"),
785    ),
786    (
787        "fs",
788        "snapshot",
789        SchemaKind::Request,
790        include_str!("../schemas/fs/snapshot.request.json"),
791    ),
792    (
793        "fs",
794        "snapshot",
795        SchemaKind::Response,
796        include_str!("../schemas/fs/snapshot.response.json"),
797    ),
798    (
799        "fs",
800        "restore",
801        SchemaKind::Request,
802        include_str!("../schemas/fs/restore.request.json"),
803    ),
804    (
805        "fs",
806        "restore",
807        SchemaKind::Response,
808        include_str!("../schemas/fs/restore.response.json"),
809    ),
810    (
811        "fs",
812        "list_snapshots",
813        SchemaKind::Request,
814        include_str!("../schemas/fs/list_snapshots.request.json"),
815    ),
816    (
817        "fs",
818        "list_snapshots",
819        SchemaKind::Response,
820        include_str!("../schemas/fs/list_snapshots.response.json"),
821    ),
822    (
823        "fs",
824        "drop_snapshot",
825        SchemaKind::Request,
826        include_str!("../schemas/fs/drop_snapshot.request.json"),
827    ),
828    (
829        "fs",
830        "drop_snapshot",
831        SchemaKind::Response,
832        include_str!("../schemas/fs/drop_snapshot.response.json"),
833    ),
834    // fs_watch/
835    (
836        "fs_watch",
837        "subscribe",
838        SchemaKind::Request,
839        include_str!("../schemas/fs_watch/subscribe.request.json"),
840    ),
841    (
842        "fs_watch",
843        "subscribe",
844        SchemaKind::Response,
845        include_str!("../schemas/fs_watch/subscribe.response.json"),
846    ),
847    (
848        "fs_watch",
849        "unsubscribe",
850        SchemaKind::Request,
851        include_str!("../schemas/fs_watch/unsubscribe.request.json"),
852    ),
853    (
854        "fs_watch",
855        "unsubscribe",
856        SchemaKind::Response,
857        include_str!("../schemas/fs_watch/unsubscribe.response.json"),
858    ),
859    // tools/
860    (
861        "tools",
862        "search",
863        SchemaKind::Request,
864        include_str!("../schemas/tools/search.request.json"),
865    ),
866    (
867        "tools",
868        "search",
869        SchemaKind::Response,
870        include_str!("../schemas/tools/search.response.json"),
871    ),
872    (
873        "tools",
874        "read_file",
875        SchemaKind::Request,
876        include_str!("../schemas/tools/read_file.request.json"),
877    ),
878    (
879        "tools",
880        "read_file",
881        SchemaKind::Response,
882        include_str!("../schemas/tools/read_file.response.json"),
883    ),
884    (
885        "tools",
886        "write_file",
887        SchemaKind::Request,
888        include_str!("../schemas/tools/write_file.request.json"),
889    ),
890    (
891        "tools",
892        "write_file",
893        SchemaKind::Response,
894        include_str!("../schemas/tools/write_file.response.json"),
895    ),
896    (
897        "tools",
898        "delete_file",
899        SchemaKind::Request,
900        include_str!("../schemas/tools/delete_file.request.json"),
901    ),
902    (
903        "tools",
904        "delete_file",
905        SchemaKind::Response,
906        include_str!("../schemas/tools/delete_file.response.json"),
907    ),
908    (
909        "tools",
910        "list_directory",
911        SchemaKind::Request,
912        include_str!("../schemas/tools/list_directory.request.json"),
913    ),
914    (
915        "tools",
916        "list_directory",
917        SchemaKind::Response,
918        include_str!("../schemas/tools/list_directory.response.json"),
919    ),
920    (
921        "tools",
922        "get_file_outline",
923        SchemaKind::Request,
924        include_str!("../schemas/tools/get_file_outline.request.json"),
925    ),
926    (
927        "tools",
928        "get_file_outline",
929        SchemaKind::Response,
930        include_str!("../schemas/tools/get_file_outline.response.json"),
931    ),
932    (
933        "tools",
934        "git",
935        SchemaKind::Request,
936        include_str!("../schemas/tools/git.request.json"),
937    ),
938    (
939        "tools",
940        "git",
941        SchemaKind::Response,
942        include_str!("../schemas/tools/git.response.json"),
943    ),
944    (
945        "tools",
946        "run_command",
947        SchemaKind::Request,
948        include_str!("../schemas/tools/run_command.request.json"),
949    ),
950    (
951        "tools",
952        "run_command",
953        SchemaKind::Response,
954        include_str!("../schemas/tools/run_command.response.json"),
955    ),
956    (
957        "tools",
958        "read_command_output",
959        SchemaKind::Request,
960        include_str!("../schemas/tools/read_command_output.request.json"),
961    ),
962    (
963        "tools",
964        "read_command_output",
965        SchemaKind::Response,
966        include_str!("../schemas/tools/read_command_output.response.json"),
967    ),
968    (
969        "tools",
970        "wait_command",
971        SchemaKind::Request,
972        include_str!("../schemas/tools/wait_command.request.json"),
973    ),
974    (
975        "tools",
976        "wait_command",
977        SchemaKind::Response,
978        include_str!("../schemas/tools/wait_command.response.json"),
979    ),
980    (
981        "tools",
982        "run_test",
983        SchemaKind::Request,
984        include_str!("../schemas/tools/run_test.request.json"),
985    ),
986    (
987        "tools",
988        "run_test",
989        SchemaKind::Response,
990        include_str!("../schemas/tools/run_test.response.json"),
991    ),
992    (
993        "tools",
994        "run_build_command",
995        SchemaKind::Request,
996        include_str!("../schemas/tools/run_build_command.request.json"),
997    ),
998    (
999        "tools",
1000        "run_build_command",
1001        SchemaKind::Response,
1002        include_str!("../schemas/tools/run_build_command.response.json"),
1003    ),
1004    (
1005        "tools",
1006        "inspect_test_results",
1007        SchemaKind::Request,
1008        include_str!("../schemas/tools/inspect_test_results.request.json"),
1009    ),
1010    (
1011        "tools",
1012        "inspect_test_results",
1013        SchemaKind::Response,
1014        include_str!("../schemas/tools/inspect_test_results.response.json"),
1015    ),
1016    (
1017        "tools",
1018        "manage_packages",
1019        SchemaKind::Request,
1020        include_str!("../schemas/tools/manage_packages.request.json"),
1021    ),
1022    (
1023        "tools",
1024        "manage_packages",
1025        SchemaKind::Response,
1026        include_str!("../schemas/tools/manage_packages.response.json"),
1027    ),
1028    (
1029        "tools",
1030        "cancel_handle",
1031        SchemaKind::Request,
1032        include_str!("../schemas/tools/cancel_handle.request.json"),
1033    ),
1034    (
1035        "tools",
1036        "cancel_handle",
1037        SchemaKind::Response,
1038        include_str!("../schemas/tools/cancel_handle.response.json"),
1039    ),
1040    (
1041        "tools",
1042        "toolchain_facts",
1043        SchemaKind::Request,
1044        include_str!("../schemas/tools/toolchain_facts.request.json"),
1045    ),
1046    (
1047        "tools",
1048        "toolchain_facts",
1049        SchemaKind::Response,
1050        include_str!("../schemas/tools/toolchain_facts.response.json"),
1051    ),
1052    (
1053        "tools",
1054        "enable",
1055        SchemaKind::Request,
1056        include_str!("../schemas/tools/enable.request.json"),
1057    ),
1058    (
1059        "tools",
1060        "enable",
1061        SchemaKind::Response,
1062        include_str!("../schemas/tools/enable.response.json"),
1063    ),
1064    // secret_store/
1065    (
1066        "secret_store",
1067        "get",
1068        SchemaKind::Request,
1069        include_str!("../schemas/secret_store/get.request.json"),
1070    ),
1071    (
1072        "secret_store",
1073        "get",
1074        SchemaKind::Response,
1075        include_str!("../schemas/secret_store/get.response.json"),
1076    ),
1077    (
1078        "secret_store",
1079        "set",
1080        SchemaKind::Request,
1081        include_str!("../schemas/secret_store/set.request.json"),
1082    ),
1083    (
1084        "secret_store",
1085        "set",
1086        SchemaKind::Response,
1087        include_str!("../schemas/secret_store/set.response.json"),
1088    ),
1089    (
1090        "secret_store",
1091        "delete",
1092        SchemaKind::Request,
1093        include_str!("../schemas/secret_store/delete.request.json"),
1094    ),
1095    (
1096        "secret_store",
1097        "delete",
1098        SchemaKind::Response,
1099        include_str!("../schemas/secret_store/delete.response.json"),
1100    ),
1101    (
1102        "secret_store",
1103        "list",
1104        SchemaKind::Request,
1105        include_str!("../schemas/secret_store/list.request.json"),
1106    ),
1107    (
1108        "secret_store",
1109        "list",
1110        SchemaKind::Response,
1111        include_str!("../schemas/secret_store/list.response.json"),
1112    ),
1113    (
1114        "embed",
1115        "similarity",
1116        SchemaKind::Request,
1117        include_str!("../schemas/embed/similarity.request.json"),
1118    ),
1119    (
1120        "embed",
1121        "similarity",
1122        SchemaKind::Response,
1123        include_str!("../schemas/embed/similarity.response.json"),
1124    ),
1125    (
1126        "embed",
1127        "top_k",
1128        SchemaKind::Request,
1129        include_str!("../schemas/embed/top_k.request.json"),
1130    ),
1131    (
1132        "embed",
1133        "top_k",
1134        SchemaKind::Response,
1135        include_str!("../schemas/embed/top_k.response.json"),
1136    ),
1137    (
1138        "embed",
1139        "vector",
1140        SchemaKind::Request,
1141        include_str!("../schemas/embed/vector.request.json"),
1142    ),
1143    (
1144        "embed",
1145        "vector",
1146        SchemaKind::Response,
1147        include_str!("../schemas/embed/vector.response.json"),
1148    ),
1149    (
1150        "embed",
1151        "info",
1152        SchemaKind::Request,
1153        include_str!("../schemas/embed/info.request.json"),
1154    ),
1155    (
1156        "embed",
1157        "info",
1158        SchemaKind::Response,
1159        include_str!("../schemas/embed/info.response.json"),
1160    ),
1161    // Extension hostlibs registered through HostlibRegistry.
1162    (
1163        "rules",
1164        "search",
1165        SchemaKind::Request,
1166        include_str!("../schemas/rules/search.request.json"),
1167    ),
1168    (
1169        "rules",
1170        "search",
1171        SchemaKind::Response,
1172        include_str!("../schemas/rules/search.response.json"),
1173    ),
1174    (
1175        "rules",
1176        "report",
1177        SchemaKind::Request,
1178        include_str!("../schemas/rules/report.request.json"),
1179    ),
1180    (
1181        "rules",
1182        "report",
1183        SchemaKind::Response,
1184        include_str!("../schemas/rules/report.response.json"),
1185    ),
1186    (
1187        "rules",
1188        "diagnostics",
1189        SchemaKind::Request,
1190        include_str!("../schemas/rules/diagnostics.request.json"),
1191    ),
1192    (
1193        "rules",
1194        "diagnostics",
1195        SchemaKind::Response,
1196        include_str!("../schemas/rules/diagnostics.response.json"),
1197    ),
1198    (
1199        "rules",
1200        "apply",
1201        SchemaKind::Request,
1202        include_str!("../schemas/rules/apply.request.json"),
1203    ),
1204    (
1205        "rules",
1206        "apply",
1207        SchemaKind::Response,
1208        include_str!("../schemas/rules/apply.response.json"),
1209    ),
1210    (
1211        "rules",
1212        "fold",
1213        SchemaKind::Request,
1214        include_str!("../schemas/rules/fold.request.json"),
1215    ),
1216    (
1217        "rules",
1218        "fold",
1219        SchemaKind::Response,
1220        include_str!("../schemas/rules/fold.response.json"),
1221    ),
1222    (
1223        "lint",
1224        "run",
1225        SchemaKind::Request,
1226        include_str!("../schemas/lint/run.request.json"),
1227    ),
1228    (
1229        "lint",
1230        "run",
1231        SchemaKind::Response,
1232        include_str!("../schemas/lint/run.response.json"),
1233    ),
1234];
1235
1236/// Look up a single schema as raw JSON text.
1237pub fn lookup(module: &str, method: &str, kind: SchemaKind) -> Option<&'static str> {
1238    SCHEMAS
1239        .iter()
1240        .find(|(m, mt, k, _)| *m == module && *mt == method && *k == kind)
1241        .map(|(_, _, _, body)| *body)
1242}
1243
1244struct CompiledSchema {
1245    module: &'static str,
1246    method: &'static str,
1247    kind: SchemaKind,
1248    body: Result<VmValue, String>,
1249}
1250
1251fn compiled_schemas() -> &'static [CompiledSchema] {
1252    static COMPILED: OnceLock<Vec<CompiledSchema>> = OnceLock::new();
1253    COMPILED.get_or_init(|| {
1254        SCHEMAS
1255            .iter()
1256            .filter(|(_, _, kind, _)| *kind == SchemaKind::Request)
1257            .map(|(module, method, kind, body)| {
1258                let body = serde_json::from_str::<serde_json::Value>(body)
1259                    .map_err(|err| format!("schema is not valid JSON: {err}"))
1260                    .map(|json| harn_vm::json_to_vm_value(&json))
1261                    .and_then(|schema| harn_vm::schema::canonicalize_json_schema(&schema));
1262                CompiledSchema {
1263                    module,
1264                    method,
1265                    kind: *kind,
1266                    body,
1267                }
1268            })
1269            .collect()
1270    })
1271}
1272
1273fn compiled_schema(
1274    module: &str,
1275    method: &str,
1276    kind: SchemaKind,
1277) -> Option<&'static CompiledSchema> {
1278    compiled_schemas()
1279        .iter()
1280        .find(|schema| schema.module == module && schema.method == method && schema.kind == kind)
1281}
1282
1283pub(crate) fn validate_request_args(
1284    builtin: &'static str,
1285    module: &'static str,
1286    method: &'static str,
1287    args: &[VmValue],
1288) -> Result<VmValue, HostlibError> {
1289    let request = normalize_request_arg(builtin, module, method, args)?;
1290    let schema = compiled_schema(module, method, SchemaKind::Request).ok_or_else(|| {
1291        HostlibError::Backend {
1292            builtin,
1293            message: format!("missing request schema for {module}.{method}"),
1294        }
1295    })?;
1296    let schema = schema
1297        .body
1298        .as_ref()
1299        .map_err(|message| HostlibError::Backend {
1300            builtin,
1301            message: format!("invalid request schema for {module}.{method}: {message}"),
1302        })?;
1303    harn_vm::schema::validate_value_against_canonical_schema(&request, schema, true).map_err(
1304        |message| HostlibError::InvalidParameter {
1305            builtin,
1306            param: "request",
1307            message,
1308        },
1309    )
1310}
1311
1312fn normalize_request_arg(
1313    builtin: &'static str,
1314    module: &'static str,
1315    method: &'static str,
1316    args: &[VmValue],
1317) -> Result<VmValue, HostlibError> {
1318    if args.len() > 1 {
1319        return Err(HostlibError::InvalidParameter {
1320            builtin,
1321            param: "request",
1322            message: format!("expected exactly one request argument, got {}", args.len()),
1323        });
1324    }
1325
1326    let first = args.first().ok_or(HostlibError::MissingParameter {
1327        builtin,
1328        param: "request",
1329    })?;
1330    match first {
1331        VmValue::Dict(map) => Ok(prune_top_level_nil_dict_fields(map)),
1332        VmValue::String(feature) if (module, method) == ("tools", "enable") => {
1333            let mut normalized = harn_vm::value::DictMap::new();
1334            normalized.put_str("feature", feature.to_string());
1335            Ok(VmValue::dict(normalized))
1336        }
1337        other => Err(HostlibError::InvalidParameter {
1338            builtin,
1339            param: "request",
1340            message: format!("expected a dict request body, got {}", other.type_name()),
1341        }),
1342    }
1343}
1344
1345fn prune_top_level_nil_dict_fields(map: &harn_vm::value::DictMap) -> VmValue {
1346    let mut pruned = harn_vm::value::DictMap::new();
1347    for (key, child) in map.iter() {
1348        if matches!(child, VmValue::Nil) {
1349            continue;
1350        }
1351        pruned.insert(key.clone(), child.clone());
1352    }
1353    VmValue::dict_map(pruned)
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358    use std::sync::Arc;
1359
1360    use harn_vm::VmValue;
1361
1362    use super::*;
1363
1364    #[test]
1365    fn request_validation_prunes_nil_optional_fields() {
1366        let request = VmValue::dict([
1367            ("session_id", VmValue::string("session-1")),
1368            ("scope_id", VmValue::string("scope-1")),
1369            ("root", VmValue::Nil),
1370        ]);
1371
1372        let validated = validate_request_args("hostlib_fs_snapshot", "fs", "snapshot", &[request])
1373            .expect("nil optional root should be omitted before schema validation");
1374        let fields = validated.as_dict().expect("validated request is a dict");
1375        assert!(fields.get("root").is_none());
1376        assert_eq!(
1377            fields.get("session_id").map(VmValue::display),
1378            Some("session-1".to_string())
1379        );
1380    }
1381
1382    #[test]
1383    fn run_command_request_schema_accepts_env_remove() {
1384        // Regression pin: `env_remove` is part of the run_command request
1385        // surface (used by embedders to strip inherited observability vars
1386        // like HARN_EVENT_LOG_DIR from child processes). The v0.10.1 schema
1387        // rollout briefly rejected it as an unknown key, which made every
1388        // downstream shelled command spawn-fail.
1389        let request = VmValue::dict([
1390            (
1391                "argv",
1392                VmValue::List(Arc::new(vec![VmValue::string("env")])),
1393            ),
1394            (
1395                "env_remove",
1396                VmValue::List(Arc::new(vec![VmValue::string("HARN_EVENT_LOG_DIR")])),
1397            ),
1398        ]);
1399
1400        validate_request_args(
1401            "hostlib_tools_run_command",
1402            "tools",
1403            "run_command",
1404            &[request],
1405        )
1406        .expect("env_remove must be a valid run_command request field");
1407    }
1408
1409    #[test]
1410    fn request_validation_does_not_prune_nested_nil_fields() {
1411        let request = VmValue::dict([
1412            (
1413                "argv",
1414                VmValue::List(Arc::new(vec![VmValue::string("env")])),
1415            ),
1416            ("env", VmValue::dict([("FOO", VmValue::Nil)])),
1417        ]);
1418
1419        let err = validate_request_args(
1420            "hostlib_tools_run_command",
1421            "tools",
1422            "run_command",
1423            &[request],
1424        )
1425        .expect_err("nested nil map values must remain visible to schema validation");
1426        match err {
1427            HostlibError::InvalidParameter { message, .. } => {
1428                assert!(
1429                    message.contains("env") && message.contains("string"),
1430                    "nested env nil should fail as a non-string value, got: {message}"
1431                );
1432            }
1433            other => panic!("expected request validation error, got {other:?}"),
1434        }
1435    }
1436
1437    #[test]
1438    fn dry_run_request_schema_allows_handler_rejected_plan_ops() {
1439        let unknown = VmValue::dict([
1440            ("op", VmValue::string("blow_up_the_world")),
1441            ("path", VmValue::string("multi.txt")),
1442        ]);
1443        let missing_op = VmValue::dict_map(harn_vm::value::DictMap::new());
1444        let non_string_op = VmValue::dict([("op", VmValue::Int(1))]);
1445        let request = VmValue::dict([(
1446            "plan",
1447            VmValue::List(Arc::new(vec![unknown, missing_op, non_string_op])),
1448        )]);
1449
1450        validate_request_args("hostlib_ast_dry_run", "ast", "dry_run", &[request]).expect(
1451            "dry_run unknown/missing/non-string ops must reach the handler for structured rejection",
1452        );
1453    }
1454
1455    #[test]
1456    fn extension_hostlib_rules_search_has_request_schema() {
1457        let request = VmValue::dict([
1458            (
1459                "rule",
1460                VmValue::string(
1461                    "id = \"noop\"\nlanguage = \"typescript\"\n[rule]\npattern = \"$X\"",
1462                ),
1463            ),
1464            ("source", VmValue::string("foo();")),
1465            ("language", VmValue::string("typescript")),
1466        ]);
1467
1468        validate_request_args("hostlib_rules_search", "rules", "search", &[request])
1469            .expect("rules.search should be covered by the shared hostlib schema catalog");
1470    }
1471}