1use std::collections::{BTreeMap, BTreeSet};
11
12use harn_builtin_meta::CapabilityId;
13
14use super::{
15 all_builtin_manifest, builtin_manifest_entry, capability_method_manifest_entry,
16 harness_method_for_builtin, stdlib_probe_vm,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum HarnessBuiltinArgumentMigration {
22 Forward,
24 RequestRecord(&'static [&'static str]),
26 CallThenProperty(&'static str),
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct HarnessBuiltinMigration {
35 pub capability: harn_builtin_meta::CapabilityId,
36 pub method: &'static str,
37 pub arguments: HarnessBuiltinArgumentMigration,
38}
39
40pub fn harness_migration_for_builtin(name: &str) -> Option<HarnessBuiltinMigration> {
48 if let Some((capability, method)) = harness_method_for_builtin(name) {
49 return Some(HarnessBuiltinMigration {
50 capability,
51 method,
52 arguments: HarnessBuiltinArgumentMigration::Forward,
53 });
54 }
55 use harn_builtin_meta::CapabilityId;
56 use HarnessBuiltinArgumentMigration::{CallThenProperty, Forward, RequestRecord};
57 let request_record = |method, fields| HarnessBuiltinMigration {
58 capability: CapabilityId::Project,
59 method,
60 arguments: RequestRecord(fields),
61 };
62 let projection = |capability, method, property| HarnessBuiltinMigration {
63 capability,
64 method,
65 arguments: CallThenProperty(property),
66 };
67 let forward = |capability, method| HarnessBuiltinMigration {
68 capability,
69 method,
70 arguments: Forward,
71 };
72 let (method, fields): (&'static str, &'static [&'static str]) = match name {
73 "metadata_get" | "metadata_resolve" => ("metadata_get", &["dir", "namespace"]),
74 "metadata_set" => ("metadata_set", &["dir", "namespace", "data"]),
75 "metadata_entries" => ("metadata_entries", &["namespace"]),
76 "metadata_save" => ("metadata_save", &[]),
77 "metadata_stale" => ("metadata_stale", &["dir"]),
78 "metadata_refresh_hashes" => ("metadata_refresh_hashes", &[]),
79 "metadata_status" => ("metadata_status", &["namespace"]),
80 "path_metadata_get" => ("path_metadata_get", &["path", "namespace", "options"]),
81 "path_metadata_set" => (
82 "path_metadata_set",
83 &["path", "namespace", "data", "options"],
84 ),
85 "path_metadata_entries" => ("path_metadata_entries", &["namespace", "options"]),
86 "platform" => return Some(projection(CapabilityId::System, "platform", "os")),
87 "arch" => return Some(projection(CapabilityId::System, "platform", "arch")),
88 "username" => return Some(projection(CapabilityId::System, "identity", "username")),
89 "hostname" => return Some(projection(CapabilityId::System, "identity", "hostname")),
90 "pid" => return Some(projection(CapabilityId::System, "identity", "pid")),
91 "execution_root" => {
92 return Some(projection(
93 CapabilityId::Fs,
94 "runtime_paths",
95 "execution_root",
96 ));
97 }
98 "asset_root" => {
99 return Some(projection(CapabilityId::Fs, "runtime_paths", "asset_root"));
100 }
101 "home_dir" => return Some(forward(CapabilityId::Fs, "home_dir")),
102 "runtime_paths" => return Some(forward(CapabilityId::Fs, "runtime_paths")),
103 "source_dir" => return Some(forward(CapabilityId::Fs, "source_dir")),
104 "project_root" => return Some(forward(CapabilityId::Fs, "project_root")),
105 "date_iso" => return Some(forward(CapabilityId::Clock, "date_iso")),
106 "term_width" => return Some(forward(CapabilityId::Term, "width")),
107 "term_height" => return Some(forward(CapabilityId::Term, "height")),
108 "security_policy" => {
109 return Some(forward(CapabilityId::System, "security_policy"));
110 }
111 "security_stamp_directive" => {
112 return Some(forward(CapabilityId::System, "security_stamp_directive"));
113 }
114 "security_verify_directive" => {
115 return Some(forward(CapabilityId::System, "security_verify_directive"));
116 }
117 "llm_catalog" => return Some(forward(CapabilityId::Llm, "catalog")),
118 "llm_catalog_refresh" => {
119 return Some(forward(CapabilityId::Llm, "catalog_refresh"));
120 }
121 "llm_provider_status" => return Some(forward(CapabilityId::Llm, "providers")),
122 "llm_session_cost" => return Some(forward(CapabilityId::Llm, "session_cost")),
123 "llm_budget" => return Some(forward(CapabilityId::Llm, "budget")),
124 "llm_budget_remaining" => {
125 return Some(forward(CapabilityId::Llm, "budget_remaining"));
126 }
127 "transport_mock_clear" => {
128 return Some(forward(CapabilityId::Testing, "transport_mock_clear"));
129 }
130 "transport_mock_calls" => {
131 return Some(forward(CapabilityId::Testing, "transport_mock_calls"));
132 }
133 "sse_mock" => return Some(forward(CapabilityId::Testing, "sse_mock")),
134 "sse_server_mock_receive" => {
135 return Some(forward(CapabilityId::Testing, "sse_server_mock_receive"));
136 }
137 "sse_server_mock_disconnect" => {
138 return Some(forward(CapabilityId::Testing, "sse_server_mock_disconnect"));
139 }
140 "websocket_mock" => return Some(forward(CapabilityId::Testing, "websocket_mock")),
141 "secret_get" => return Some(forward(CapabilityId::Secrets, "read")),
145 "mock_time" => return Some(forward(CapabilityId::Testing, "clock_set")),
149 "unmock_time" => return Some(forward(CapabilityId::Testing, "clock_reset")),
150 "advance_time" => return Some(forward(CapabilityId::Testing, "clock_advance")),
151 "mock_stdin" => return Some(forward(CapabilityId::Testing, "stdin_set")),
152 "unmock_stdin" => return Some(forward(CapabilityId::Testing, "stdin_reset")),
153 "mock_tty" => return Some(forward(CapabilityId::Testing, "tty_set")),
154 "unmock_tty" => return Some(forward(CapabilityId::Testing, "tty_reset")),
155 "host_mock_push_scope" => return Some(forward(CapabilityId::Testing, "push_scope")),
156 "host_mock_pop_scope" => return Some(forward(CapabilityId::Testing, "pop_scope")),
157 "host_mock_calls" => return Some(forward(CapabilityId::Testing, "calls")),
158 "llm_mock" => return Some(forward(CapabilityId::Llm, "mock_enqueue")),
159 "render_string" => return Some(forward(CapabilityId::Fs, "render_template")),
160 "render_with_provenance" => {
161 return Some(forward(CapabilityId::Fs, "render_prompt_with_provenance"));
162 }
163 "crypto_random_bytes" => return Some(forward(CapabilityId::Random, "bytes")),
164 "emit_channel" => return Some(forward(CapabilityId::Channels, "append")),
165 "flush_trigger_aggregations" => {
166 return Some(forward(CapabilityId::Channels, "flush_aggregations"));
167 }
168 "channel_ack" => return Some(forward(CapabilityId::Channels, "ack")),
170 "channel_events" => return Some(forward(CapabilityId::Channels, "events")),
171 "channel_subscribe" => return Some(forward(CapabilityId::Channels, "subscribe")),
172 "channel_consumer_cursor" => {
173 return Some(forward(CapabilityId::Channels, "consumer_cursor"));
174 }
175 "pg_connect" => return Some(forward(CapabilityId::Postgres, "connect")),
176 "pg_pool" => return Some(forward(CapabilityId::Postgres, "pool")),
177 _ => {
178 return derived_capability_owner(name)
179 .map(|(capability, method)| forward(capability, method));
180 }
181 };
182 Some(request_record(method, fields))
183}
184
185struct CapabilityMethodIndex {
196 bridged_owner: BTreeMap<&'static str, Option<CapabilityId>>,
200 methods_by_capability: BTreeMap<CapabilityId, BTreeSet<&'static str>>,
201}
202
203fn capability_method_index() -> &'static CapabilityMethodIndex {
204 static INDEX: std::sync::OnceLock<CapabilityMethodIndex> = std::sync::OnceLock::new();
205 INDEX.get_or_init(|| {
206 let mut index = CapabilityMethodIndex {
207 bridged_owner: BTreeMap::new(),
208 methods_by_capability: BTreeMap::new(),
209 };
210 for entry in all_builtin_manifest() {
211 if let harn_builtin_meta::BuiltinExposure::HarnessMethod { capability, method } =
212 entry.contract.exposure
213 {
214 index
215 .methods_by_capability
216 .entry(capability)
217 .or_default()
218 .insert(method);
219 }
220 }
221 for (capability, method) in stdlib_probe_vm().capability_method_names() {
222 let method: &'static str = Box::leak(method.into_boxed_str());
223 index
224 .bridged_owner
225 .entry(method)
226 .and_modify(|owner| {
227 if *owner != Some(capability) {
228 *owner = None;
229 }
230 })
231 .or_insert(Some(capability));
232 index
233 .methods_by_capability
234 .entry(capability)
235 .or_default()
236 .insert(method);
237 }
238 index
239 })
240}
241
242fn derived_capability_owner(name: &str) -> Option<(CapabilityId, &'static str)> {
262 if is_source_visible_global(name) {
263 return None;
264 }
265 let index = capability_method_index();
266 if let Some((method, Some(owner))) = index.bridged_owner.get_key_value(name) {
267 return Some((*owner, method));
268 }
269
270 let mut candidates: Vec<(CapabilityId, &'static str)> = Vec::new();
271 let unprefixed = name.strip_prefix("hostlib_").unwrap_or(name);
272 for (capability, methods) in &index.methods_by_capability {
273 if let Some(method) = methods.get(name) {
274 candidates.push((*capability, method));
275 }
276 for prefix in [
277 capability.field_name().to_string(),
278 snake_case(capability.variant_name()),
279 ] {
280 let Some(rest) = unprefixed
281 .strip_prefix(&prefix)
282 .and_then(|rest| rest.strip_prefix('_'))
283 else {
284 continue;
285 };
286 for method in [Some(rest), rest.strip_prefix("session_")]
289 .into_iter()
290 .flatten()
291 .filter_map(|method| methods.get(method))
292 {
293 candidates.push((*capability, method));
294 }
295 }
296 }
297 candidates.sort_unstable();
298 candidates.dedup();
299 if candidates.len() > 1 {
300 candidates
301 .retain(|(capability, method)| takes_the_same_parameters(name, *capability, method));
302 }
303 match candidates.as_slice() {
304 [only] => Some(*only),
305 _ => None,
306 }
307}
308
309fn takes_the_same_parameters(removed_global: &str, capability: CapabilityId, method: &str) -> bool {
314 let Some(before) = builtin_manifest_entry(removed_global) else {
315 return false;
316 };
317 let Some(after) = capability_method_manifest_entry(capability, method) else {
318 return false;
319 };
320 let names = |entry: &'static harn_builtin_registry::BuiltinManifestEntry| {
321 entry
322 .signature
323 .params
324 .iter()
325 .map(|param| param.name)
326 .collect::<Vec<_>>()
327 };
328 names(before) == names(after)
329}
330
331fn snake_case(camel: &str) -> String {
332 let mut out = String::with_capacity(camel.len() + 2);
333 for (index, ch) in camel.char_indices() {
334 if ch.is_ascii_uppercase() && index > 0 {
335 out.push('_');
336 }
337 out.push(ch.to_ascii_lowercase());
338 }
339 out
340}
341
342fn is_source_visible_global(name: &str) -> bool {
344 builtin_manifest_entry(name).is_some_and(|entry| {
345 matches!(
346 entry.contract.exposure,
347 harn_builtin_meta::BuiltinExposure::PureGlobal
348 | harn_builtin_meta::BuiltinExposure::CapabilityFunction { .. }
349 )
350 })
351}
352#[cfg(test)]
353mod registered_capability_migration_tests {
354 use harn_builtin_meta::CapabilityId;
355
356 use super::{
357 all_builtin_manifest, harness_migration_for_builtin, HarnessBuiltinArgumentMigration,
358 HarnessBuiltinMigration,
359 };
360 use crate::stdlib::stdlib_probe_vm;
361
362 #[test]
363 fn migration_recipes_follow_names_that_moved_onto_a_handle() {
364 let forward = |capability, method| {
365 Some(HarnessBuiltinMigration {
366 capability,
367 method,
368 arguments: HarnessBuiltinArgumentMigration::Forward,
369 })
370 };
371 assert_eq!(
373 harness_migration_for_builtin("exit"),
374 forward(CapabilityId::Runtime, "exit")
375 );
376 assert_eq!(
378 harness_migration_for_builtin("hostlib_code_index_rebuild"),
379 forward(CapabilityId::CodeIndex, "rebuild")
380 );
381 assert_eq!(
383 harness_migration_for_builtin("agent_session_open"),
384 forward(CapabilityId::Agent, "open")
385 );
386 assert_eq!(harness_migration_for_builtin("len"), None);
388 }
389
390 fn has_a_repair(name: &str) -> bool {
396 use harn_parser::diagnostic::{
397 harness_clock_replacement, harness_env_replacement, harness_fs_replacement,
398 harness_net_replacement, harness_random_replacement, harness_stdio_replacement,
399 };
400 harness_migration_for_builtin(name).is_some()
401 || harness_clock_replacement(name).is_some()
402 || harness_stdio_replacement(name).is_some()
403 || harness_fs_replacement(name).is_some()
404 || harness_env_replacement(name).is_some()
405 || harness_random_replacement(name).is_some()
406 || harness_net_replacement(name).is_some()
407 }
408
409 const RUNTIME_PLUMBING: &[&str] = &[
416 "exec_at_opts",
417 "exec_opts",
418 "host_tool_call",
419 "host_tool_list",
420 "invalidate_facts",
421 "llm_mock_known_scopes",
422 "llm_mock_load_jsonl",
423 "llm_mock_receipts",
424 "render",
425 ];
426
427 #[test]
432 fn every_runtime_internal_builtin_is_migrated_or_named_as_plumbing() {
433 use harn_builtin_meta::BuiltinExposure;
434
435 let offenders = all_builtin_manifest()
436 .iter()
437 .filter(|entry| entry.is_canonical())
438 .filter(|entry| matches!(entry.contract.exposure, BuiltinExposure::RuntimeInternal))
439 .filter(|entry| !entry.name.starts_with("__"))
440 .filter(|entry| !RUNTIME_PLUMBING.contains(&entry.name))
441 .filter(|entry| !has_a_repair(entry.name))
442 .map(|entry| entry.name)
443 .collect::<Vec<_>>();
444
445 assert!(
446 offenders.is_empty(),
447 "these globals moved onto a handle but report no repair: {offenders:?}"
448 );
449 }
450
451 #[test]
455 fn every_uniquely_owned_capability_method_has_a_migration() {
456 let vm = stdlib_probe_vm();
457 let declared: std::collections::BTreeSet<String> = super::all_builtin_manifest()
458 .iter()
459 .map(|entry| entry.name.to_string())
460 .collect();
461 let mut owners: std::collections::BTreeMap<String, std::collections::BTreeSet<_>> =
462 std::collections::BTreeMap::new();
463 for (capability, method) in vm.capability_method_names() {
464 owners.entry(method).or_default().insert(capability);
465 }
466
467 let missing: Vec<_> = owners
468 .iter()
469 .filter(|(method, capabilities)| {
470 capabilities.len() == 1
471 && !declared.contains(*method)
472 && harness_migration_for_builtin(method).is_none()
473 })
474 .map(|(method, _)| method.clone())
475 .collect();
476 assert!(
477 missing.is_empty(),
478 "capability methods without a migration recipe: {missing:?}"
479 );
480 }
481
482 #[test]
483 fn runtime_registered_store_methods_migrate_to_their_owning_capability() {
484 for method in ["store_get", "store_set", "store_delete", "store_list"] {
485 let migration =
486 harness_migration_for_builtin(method).expect("store method has a migration");
487 assert_eq!(
488 migration.capability,
489 harn_builtin_meta::CapabilityId::Runtime
490 );
491 assert_eq!(migration.method, method);
492 assert_eq!(
493 migration.arguments,
494 HarnessBuiltinArgumentMigration::Forward
495 );
496 }
497 }
498
499 #[test]
502 fn ambiguously_owned_methods_have_no_migration() {
503 let vm = stdlib_probe_vm();
504 let mut owners: std::collections::BTreeMap<String, std::collections::BTreeSet<_>> =
505 std::collections::BTreeMap::new();
506 for (capability, method) in vm.capability_method_names() {
507 owners.entry(method).or_default().insert(capability);
508 }
509 let Some((method, _)) = owners
510 .iter()
511 .find(|(method, capabilities)| {
512 capabilities.len() > 1 && super::harness_method_for_builtin(method).is_none()
513 })
514 .map(|(method, capabilities)| (method.clone(), capabilities.clone()))
515 else {
516 return;
517 };
518 assert!(
519 super::derived_capability_owner(&method).is_none(),
520 "`{method}` is owned by several capabilities and must not resolve to one"
521 );
522 }
523}