use std::collections::{BTreeMap, BTreeSet};
use harn_builtin_meta::CapabilityId;
use super::{
all_builtin_manifest, builtin_manifest_entry, capability_method_manifest_entry,
harness_method_for_builtin, stdlib_probe_vm,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HarnessBuiltinArgumentMigration {
Forward,
RequestRecord(&'static [&'static str]),
CallThenProperty(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HarnessBuiltinMigration {
pub capability: harn_builtin_meta::CapabilityId,
pub method: &'static str,
pub arguments: HarnessBuiltinArgumentMigration,
}
pub fn harness_migration_for_builtin(name: &str) -> Option<HarnessBuiltinMigration> {
if let Some((capability, method)) = harness_method_for_builtin(name) {
return Some(HarnessBuiltinMigration {
capability,
method,
arguments: HarnessBuiltinArgumentMigration::Forward,
});
}
use harn_builtin_meta::CapabilityId;
use HarnessBuiltinArgumentMigration::{CallThenProperty, Forward, RequestRecord};
let request_record = |method, fields| HarnessBuiltinMigration {
capability: CapabilityId::Project,
method,
arguments: RequestRecord(fields),
};
let projection = |capability, method, property| HarnessBuiltinMigration {
capability,
method,
arguments: CallThenProperty(property),
};
let forward = |capability, method| HarnessBuiltinMigration {
capability,
method,
arguments: Forward,
};
let (method, fields): (&'static str, &'static [&'static str]) = match name {
"metadata_get" | "metadata_resolve" => ("metadata_get", &["dir", "namespace"]),
"metadata_set" => ("metadata_set", &["dir", "namespace", "data"]),
"metadata_entries" => ("metadata_entries", &["namespace"]),
"metadata_save" => ("metadata_save", &[]),
"metadata_stale" => ("metadata_stale", &["dir"]),
"metadata_refresh_hashes" => ("metadata_refresh_hashes", &[]),
"metadata_status" => ("metadata_status", &["namespace"]),
"path_metadata_get" => ("path_metadata_get", &["path", "namespace", "options"]),
"path_metadata_set" => (
"path_metadata_set",
&["path", "namespace", "data", "options"],
),
"path_metadata_entries" => ("path_metadata_entries", &["namespace", "options"]),
"platform" => return Some(projection(CapabilityId::System, "platform", "os")),
"arch" => return Some(projection(CapabilityId::System, "platform", "arch")),
"username" => return Some(projection(CapabilityId::System, "identity", "username")),
"hostname" => return Some(projection(CapabilityId::System, "identity", "hostname")),
"pid" => return Some(projection(CapabilityId::System, "identity", "pid")),
"execution_root" => {
return Some(projection(
CapabilityId::Fs,
"runtime_paths",
"execution_root",
));
}
"asset_root" => {
return Some(projection(CapabilityId::Fs, "runtime_paths", "asset_root"));
}
"home_dir" => return Some(forward(CapabilityId::Fs, "home_dir")),
"runtime_paths" => return Some(forward(CapabilityId::Fs, "runtime_paths")),
"source_dir" => return Some(forward(CapabilityId::Fs, "source_dir")),
"project_root" => return Some(forward(CapabilityId::Fs, "project_root")),
"date_iso" => return Some(forward(CapabilityId::Clock, "date_iso")),
"term_width" => return Some(forward(CapabilityId::Term, "width")),
"term_height" => return Some(forward(CapabilityId::Term, "height")),
"security_policy" => {
return Some(forward(CapabilityId::System, "security_policy"));
}
"security_stamp_directive" => {
return Some(forward(CapabilityId::System, "security_stamp_directive"));
}
"security_verify_directive" => {
return Some(forward(CapabilityId::System, "security_verify_directive"));
}
"llm_catalog" => return Some(forward(CapabilityId::Llm, "catalog")),
"llm_catalog_refresh" => {
return Some(forward(CapabilityId::Llm, "catalog_refresh"));
}
"llm_provider_status" => return Some(forward(CapabilityId::Llm, "providers")),
"llm_session_cost" => return Some(forward(CapabilityId::Llm, "session_cost")),
"llm_budget" => return Some(forward(CapabilityId::Llm, "budget")),
"llm_budget_remaining" => {
return Some(forward(CapabilityId::Llm, "budget_remaining"));
}
"http_mock" => return Some(forward(CapabilityId::Testing, "http_mock")),
"http_mock_clear" => return Some(forward(CapabilityId::Testing, "http_mock_clear")),
"http_mock_calls" => return Some(forward(CapabilityId::Testing, "http_mock_calls")),
"egress_policy" => return Some(forward(CapabilityId::Net, "egress_policy")),
"transport_mock_clear" => {
return Some(forward(CapabilityId::Testing, "transport_mock_clear"));
}
"transport_mock_calls" => {
return Some(forward(CapabilityId::Testing, "transport_mock_calls"));
}
"sse_mock" => return Some(forward(CapabilityId::Testing, "sse_mock")),
"sse_server_mock_receive" => {
return Some(forward(CapabilityId::Testing, "sse_server_mock_receive"));
}
"sse_server_mock_disconnect" => {
return Some(forward(CapabilityId::Testing, "sse_server_mock_disconnect"));
}
"websocket_mock" => return Some(forward(CapabilityId::Testing, "websocket_mock")),
"secret_get" => return Some(forward(CapabilityId::Secrets, "read")),
"mock_time" => return Some(forward(CapabilityId::Testing, "clock_set")),
"unmock_time" => return Some(forward(CapabilityId::Testing, "clock_reset")),
"advance_time" => return Some(forward(CapabilityId::Testing, "clock_advance")),
"mock_stdin" => return Some(forward(CapabilityId::Testing, "stdin_set")),
"unmock_stdin" => return Some(forward(CapabilityId::Testing, "stdin_reset")),
"mock_tty" => return Some(forward(CapabilityId::Testing, "tty_set")),
"unmock_tty" => return Some(forward(CapabilityId::Testing, "tty_reset")),
"host_mock_push_scope" => return Some(forward(CapabilityId::Testing, "push_scope")),
"host_mock_pop_scope" => return Some(forward(CapabilityId::Testing, "pop_scope")),
"host_mock_calls" => return Some(forward(CapabilityId::Testing, "calls")),
"llm_mock" => return Some(forward(CapabilityId::Llm, "mock_enqueue")),
"render_string" => return Some(forward(CapabilityId::Fs, "render_template")),
"render_with_provenance" => {
return Some(forward(CapabilityId::Fs, "render_prompt_with_provenance"));
}
"crypto_random_bytes" => return Some(forward(CapabilityId::Random, "bytes")),
"emit_channel" => return Some(forward(CapabilityId::Channels, "append")),
"flush_trigger_aggregations" => {
return Some(forward(CapabilityId::Channels, "flush_aggregations"));
}
"channel_ack" => return Some(forward(CapabilityId::Channels, "ack")),
"channel_events" => return Some(forward(CapabilityId::Channels, "events")),
"channel_subscribe" => return Some(forward(CapabilityId::Channels, "subscribe")),
"channel_consumer_cursor" => {
return Some(forward(CapabilityId::Channels, "consumer_cursor"));
}
"pg_connect" => return Some(forward(CapabilityId::Postgres, "connect")),
"pg_pool" => return Some(forward(CapabilityId::Postgres, "pool")),
_ => {
return derived_capability_owner(name)
.map(|(capability, method)| forward(capability, method));
}
};
Some(request_record(method, fields))
}
struct CapabilityMethodIndex {
bridged_owner: BTreeMap<&'static str, Option<CapabilityId>>,
methods_by_capability: BTreeMap<CapabilityId, BTreeSet<&'static str>>,
}
fn capability_method_index() -> &'static CapabilityMethodIndex {
static INDEX: std::sync::OnceLock<CapabilityMethodIndex> = std::sync::OnceLock::new();
INDEX.get_or_init(|| {
let mut index = CapabilityMethodIndex {
bridged_owner: BTreeMap::new(),
methods_by_capability: BTreeMap::new(),
};
for entry in all_builtin_manifest() {
if let harn_builtin_meta::BuiltinExposure::HarnessMethod { capability, method } =
entry.contract.exposure
{
index
.methods_by_capability
.entry(capability)
.or_default()
.insert(method);
}
}
for (capability, method) in stdlib_probe_vm().capability_method_names() {
let method: &'static str = Box::leak(method.into_boxed_str());
index
.bridged_owner
.entry(method)
.and_modify(|owner| {
if *owner != Some(capability) {
*owner = None;
}
})
.or_insert(Some(capability));
index
.methods_by_capability
.entry(capability)
.or_default()
.insert(method);
}
index
})
}
fn derived_capability_owner(name: &str) -> Option<(CapabilityId, &'static str)> {
if is_source_visible_global(name) {
return None;
}
let index = capability_method_index();
if let Some((method, Some(owner))) = index.bridged_owner.get_key_value(name) {
return Some((*owner, method));
}
let mut candidates: Vec<(CapabilityId, &'static str)> = Vec::new();
let unprefixed = name.strip_prefix("hostlib_").unwrap_or(name);
for (capability, methods) in &index.methods_by_capability {
if let Some(method) = methods.get(name) {
candidates.push((*capability, method));
}
for prefix in [
capability.field_name().to_string(),
snake_case(capability.variant_name()),
] {
let Some(rest) = unprefixed
.strip_prefix(&prefix)
.and_then(|rest| rest.strip_prefix('_'))
else {
continue;
};
for method in [Some(rest), rest.strip_prefix("session_")]
.into_iter()
.flatten()
.filter_map(|method| methods.get(method))
{
candidates.push((*capability, method));
}
}
}
candidates.sort_unstable();
candidates.dedup();
if candidates.len() > 1 {
candidates
.retain(|(capability, method)| takes_the_same_parameters(name, *capability, method));
}
match candidates.as_slice() {
[only] => Some(*only),
_ => None,
}
}
fn takes_the_same_parameters(removed_global: &str, capability: CapabilityId, method: &str) -> bool {
let Some(before) = builtin_manifest_entry(removed_global) else {
return false;
};
let Some(after) = capability_method_manifest_entry(capability, method) else {
return false;
};
let names = |entry: &'static harn_builtin_registry::BuiltinManifestEntry| {
entry
.signature
.params
.iter()
.map(|param| param.name)
.collect::<Vec<_>>()
};
names(before) == names(after)
}
fn snake_case(camel: &str) -> String {
let mut out = String::with_capacity(camel.len() + 2);
for (index, ch) in camel.char_indices() {
if ch.is_ascii_uppercase() && index > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
}
out
}
fn is_source_visible_global(name: &str) -> bool {
builtin_manifest_entry(name).is_some_and(|entry| {
matches!(
entry.contract.exposure,
harn_builtin_meta::BuiltinExposure::PureGlobal
| harn_builtin_meta::BuiltinExposure::CapabilityFunction { .. }
)
})
}
#[cfg(test)]
mod registered_capability_migration_tests {
use harn_builtin_meta::CapabilityId;
use super::{
all_builtin_manifest, harness_migration_for_builtin, HarnessBuiltinArgumentMigration,
HarnessBuiltinMigration,
};
use crate::stdlib::stdlib_probe_vm;
#[test]
fn migration_recipes_follow_names_that_moved_onto_a_handle() {
let forward = |capability, method| {
Some(HarnessBuiltinMigration {
capability,
method,
arguments: HarnessBuiltinArgumentMigration::Forward,
})
};
assert_eq!(
harness_migration_for_builtin("exit"),
forward(CapabilityId::Runtime, "exit")
);
assert_eq!(
harness_migration_for_builtin("hostlib_code_index_rebuild"),
forward(CapabilityId::CodeIndex, "rebuild")
);
assert_eq!(
harness_migration_for_builtin("agent_session_open"),
forward(CapabilityId::Agent, "open")
);
assert_eq!(harness_migration_for_builtin("len"), None);
}
#[test]
fn the_whole_mock_family_says_where_it_went() {
let forward = |capability, method| {
Some(HarnessBuiltinMigration {
capability,
method,
arguments: HarnessBuiltinArgumentMigration::Forward,
})
};
for name in ["http_mock", "http_mock_clear", "http_mock_calls"] {
assert_eq!(
harness_migration_for_builtin(name),
forward(CapabilityId::Testing, name),
"{name} left no way back to its handle"
);
}
for name in ["transport_mock_clear", "transport_mock_calls"] {
assert_eq!(
harness_migration_for_builtin(name),
forward(CapabilityId::Testing, name),
"{name} left no way back to its handle"
);
}
assert_eq!(
harness_migration_for_builtin("egress_policy"),
forward(CapabilityId::Net, "egress_policy")
);
}
fn has_a_repair(name: &str) -> bool {
use harn_parser::diagnostic::{
harness_clock_replacement, harness_env_replacement, harness_fs_replacement,
harness_net_replacement, harness_random_replacement, harness_stdio_replacement,
};
harness_migration_for_builtin(name).is_some()
|| harness_clock_replacement(name).is_some()
|| harness_stdio_replacement(name).is_some()
|| harness_fs_replacement(name).is_some()
|| harness_env_replacement(name).is_some()
|| harness_random_replacement(name).is_some()
|| harness_net_replacement(name).is_some()
}
const RUNTIME_PLUMBING: &[&str] = &[
"exec_at_opts",
"exec_opts",
"host_tool_call",
"host_tool_list",
"invalidate_facts",
"llm_mock_known_scopes",
"llm_mock_load_jsonl",
"llm_mock_receipts",
"render",
];
#[test]
fn every_runtime_internal_builtin_is_migrated_or_named_as_plumbing() {
use harn_builtin_meta::BuiltinExposure;
let offenders = all_builtin_manifest()
.iter()
.filter(|entry| entry.is_canonical())
.filter(|entry| matches!(entry.contract.exposure, BuiltinExposure::RuntimeInternal))
.filter(|entry| !entry.name.starts_with("__"))
.filter(|entry| !RUNTIME_PLUMBING.contains(&entry.name))
.filter(|entry| !has_a_repair(entry.name))
.map(|entry| entry.name)
.collect::<Vec<_>>();
assert!(
offenders.is_empty(),
"these globals moved onto a handle but report no repair: {offenders:?}"
);
}
#[test]
fn every_uniquely_owned_capability_method_has_a_migration() {
let vm = stdlib_probe_vm();
let declared: std::collections::BTreeSet<String> = super::all_builtin_manifest()
.iter()
.map(|entry| entry.name.to_string())
.collect();
let mut owners: std::collections::BTreeMap<String, std::collections::BTreeSet<_>> =
std::collections::BTreeMap::new();
for (capability, method) in vm.capability_method_names() {
owners.entry(method).or_default().insert(capability);
}
let missing: Vec<_> = owners
.iter()
.filter(|(method, capabilities)| {
capabilities.len() == 1
&& !declared.contains(*method)
&& harness_migration_for_builtin(method).is_none()
})
.map(|(method, _)| method.clone())
.collect();
assert!(
missing.is_empty(),
"capability methods without a migration recipe: {missing:?}"
);
}
#[test]
fn runtime_registered_store_methods_migrate_to_their_owning_capability() {
for method in ["store_get", "store_set", "store_delete", "store_list"] {
let migration =
harness_migration_for_builtin(method).expect("store method has a migration");
assert_eq!(
migration.capability,
harn_builtin_meta::CapabilityId::Runtime
);
assert_eq!(migration.method, method);
assert_eq!(
migration.arguments,
HarnessBuiltinArgumentMigration::Forward
);
}
}
#[test]
fn ambiguously_owned_methods_have_no_migration() {
let vm = stdlib_probe_vm();
let mut owners: std::collections::BTreeMap<String, std::collections::BTreeSet<_>> =
std::collections::BTreeMap::new();
for (capability, method) in vm.capability_method_names() {
owners.entry(method).or_default().insert(capability);
}
let Some((method, _)) = owners
.iter()
.find(|(method, capabilities)| {
capabilities.len() > 1 && super::harness_method_for_builtin(method).is_none()
})
.map(|(method, capabilities)| (method.clone(), capabilities.clone()))
else {
return;
};
assert!(
super::derived_capability_owner(&method).is_none(),
"`{method}` is owned by several capabilities and must not resolve to one"
);
}
}