use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use std::sync::Arc;
use crate::value::{ErrorCategory, VmError, VmValue};
use super::{skill_entry_to_vm, substitute_skill_body, Skill, SubstitutionContext};
pub type SkillFetcher = Arc<dyn Fn(&str) -> Result<Skill, String> + Send + Sync>;
#[derive(Clone)]
pub struct BoundSkillRegistry {
pub registry: VmValue,
pub fetcher: SkillFetcher,
}
pub struct LoadedSkill {
pub id: String,
pub entry: BTreeMap<String, VmValue>,
pub rendered_body: String,
}
#[derive(Debug, Clone, Default)]
pub struct LoadSkillOptions {
pub session_id: Option<String>,
pub require_signature: bool,
pub model_invocation: bool,
}
thread_local! {
static CURRENT_SKILL_REGISTRY: RefCell<Option<BoundSkillRegistry>> = const { RefCell::new(None) };
}
pub fn install_current_skill_registry(
binding: Option<BoundSkillRegistry>,
) -> Option<BoundSkillRegistry> {
CURRENT_SKILL_REGISTRY.with(|slot| slot.replace(binding))
}
pub fn current_skill_registry() -> Option<BoundSkillRegistry> {
CURRENT_SKILL_REGISTRY.with(|slot| slot.borrow().clone())
}
pub fn clear_current_skill_registry() {
CURRENT_SKILL_REGISTRY.with(|slot| {
*slot.borrow_mut() = None;
});
}
pub fn skill_entry_id(entry: &BTreeMap<String, VmValue>) -> String {
let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
let namespace = entry
.get("namespace")
.map(|v| v.display())
.filter(|value| !value.is_empty());
match namespace {
Some(ns) => format!("{ns}/{name}"),
None => name,
}
}
pub fn resolve_skill_entry(
registry: &VmValue,
target: &str,
builtin_name: &str,
) -> Result<BTreeMap<String, VmValue>, String> {
let dict = registry
.as_dict()
.ok_or_else(|| format!("{builtin_name}: bound skill registry is not a dict"))?;
let skills = match dict.get("skills") {
Some(VmValue::List(list)) => list,
_ => {
return Err(format!("{builtin_name}: bound skill registry is malformed"));
}
};
let mut bare_matches: Vec<BTreeMap<String, VmValue>> = Vec::new();
for skill in skills.iter() {
let Some(entry) = skill.as_dict() else {
continue;
};
if skill_entry_id(entry) == target {
return Ok(entry.clone());
}
if entry
.get("name")
.map(|value| value.display())
.is_some_and(|name| name == target)
{
bare_matches.push(entry.clone());
}
}
match bare_matches.len() {
1 => Ok(bare_matches.remove(0)),
0 => Err(format!("skill_not_found: skill '{target}' not found")),
_ => Err(format!(
"skill '{target}' is ambiguous; use the fully qualified id from the catalog"
)),
}
}
fn entry_has_inline_body(entry: &BTreeMap<String, VmValue>) -> bool {
entry
.get("body")
.map(|value| value.display())
.filter(|value| !value.is_empty())
.is_some()
|| entry
.get("prompt")
.map(|value| value.display())
.filter(|value| !value.is_empty())
.is_some()
}
fn body_from_entry(entry: &BTreeMap<String, VmValue>) -> String {
entry
.get("body")
.map(|value| value.display())
.filter(|value| !value.is_empty())
.or_else(|| {
entry
.get("prompt")
.map(|value| value.display())
.filter(|value| !value.is_empty())
})
.unwrap_or_default()
}
fn hydrate_skill_entry(
entry: BTreeMap<String, VmValue>,
fetcher: Option<&SkillFetcher>,
builtin_name: &str,
) -> Result<BTreeMap<String, VmValue>, String> {
if entry_has_inline_body(&entry) {
return Ok(entry);
}
let skill_id = skill_entry_id(&entry);
let Some(fetcher) = fetcher else {
return Err(format!(
"{builtin_name}: skill '{skill_id}' is not lazily loadable in this scope"
));
};
let loaded = fetcher(&skill_id)?;
match skill_entry_to_vm(&loaded) {
VmValue::Dict(dict) => {
let mut hydrated = (*dict).clone();
for (key, value) in entry {
hydrated.entry(key).or_insert(value);
}
Ok(hydrated)
}
_ => Err(format!(
"{builtin_name}: failed to hydrate skill '{skill_id}'"
)),
}
}
fn render_skill_entry(entry: &BTreeMap<String, VmValue>, session_id: Option<&str>) -> String {
let skill_dir = entry
.get("skill_dir")
.map(|value| value.display())
.filter(|value| !value.is_empty());
substitute_skill_body(
&body_from_entry(entry),
&SubstitutionContext {
arguments: Vec::new(),
skill_dir,
session_id: session_id.map(str::to_string),
extra_env: Default::default(),
},
)
}
pub fn load_bound_skill_by_name(
requested: &str,
session_id: Option<&str>,
) -> Result<LoadedSkill, String> {
load_bound_skill_by_name_with_options(
requested,
LoadSkillOptions {
session_id: session_id.map(str::to_string),
require_signature: false,
model_invocation: false,
},
)
}
pub fn load_bound_skill_by_name_with_options(
requested: &str,
options: LoadSkillOptions,
) -> Result<LoadedSkill, String> {
let Some(binding) = current_skill_registry() else {
return Err(
"load_skill: no skill registry is bound to this scope. Start the VM with discovered skills first."
.to_string(),
);
};
load_skill_from_registry_with_options(
&binding.registry,
Some(&binding.fetcher),
requested,
options,
"load_skill",
)
}
pub fn load_skill_from_registry(
registry: &VmValue,
fetcher: Option<&SkillFetcher>,
requested: &str,
session_id: Option<&str>,
builtin_name: &str,
) -> Result<LoadedSkill, String> {
load_skill_from_registry_with_options(
registry,
fetcher,
requested,
LoadSkillOptions {
session_id: session_id.map(str::to_string),
require_signature: false,
model_invocation: false,
},
builtin_name,
)
}
pub fn load_skill_from_registry_with_options(
registry: &VmValue,
fetcher: Option<&SkillFetcher>,
requested: &str,
options: LoadSkillOptions,
builtin_name: &str,
) -> Result<LoadedSkill, String> {
let entry = resolve_skill_entry(registry, requested, builtin_name)?;
let id = skill_entry_id(&entry);
if options.model_invocation && vm_bool_field(&entry, "disable_model_invocation") {
return Err(format!(
"skill_model_invocation_disabled: skill '{id}' cannot be loaded by a model"
));
}
let require_signature = options.require_signature || vm_bool_field(&entry, "require_signature");
if require_signature {
let signed = vm_provenance_bool(&entry, "signed");
let trusted = vm_provenance_bool(&entry, "trusted");
if !signed || !trusted {
record_skill_loaded_event(
options.session_id.as_deref(),
&id,
&entry,
Some("UnsignedSkillError"),
);
return Err(format!(
"UnsignedSkillError: skill '{id}' requires a trusted signature"
));
}
}
let entry = hydrate_skill_entry(entry, fetcher, builtin_name)?;
record_skill_loaded_event(options.session_id.as_deref(), &id, &entry, None);
let rendered_body = render_skill_entry(&entry, options.session_id.as_deref());
Ok(LoadedSkill {
id,
entry,
rendered_body,
})
}
fn vm_bool_field(entry: &BTreeMap<String, VmValue>, key: &str) -> bool {
matches!(entry.get(key), Some(VmValue::Bool(true)))
}
fn vm_provenance(entry: &BTreeMap<String, VmValue>) -> Option<&BTreeMap<String, VmValue>> {
entry.get("provenance").and_then(VmValue::as_dict)
}
fn vm_provenance_bool(entry: &BTreeMap<String, VmValue>, key: &str) -> bool {
vm_provenance(entry)
.and_then(|provenance| provenance.get(key))
.is_some_and(|value| matches!(value, VmValue::Bool(true)))
}
fn record_skill_loaded_event(
session_id: Option<&str>,
skill_id: &str,
entry: &BTreeMap<String, VmValue>,
error: Option<&str>,
) {
let Some(session_id) = session_id.filter(|value| !value.is_empty()) else {
return;
};
let provenance = vm_provenance(entry);
let signed = provenance
.and_then(|metadata| metadata.get("signed"))
.is_some_and(|value| matches!(value, VmValue::Bool(true)));
let trusted = provenance
.and_then(|metadata| metadata.get("trusted"))
.is_some_and(|value| matches!(value, VmValue::Bool(true)));
let mut metadata = serde_json::Map::new();
metadata.insert("skill_id".to_string(), serde_json::json!(skill_id));
metadata.insert("signed".to_string(), serde_json::json!(signed));
metadata.insert("trusted".to_string(), serde_json::json!(trusted));
if let Some(provenance) = provenance {
for key in [
"status",
"signer_fingerprint",
"skill_sha256",
"author",
"endorsements",
"trust_policy_input",
] {
if let Some(value) = provenance.get(key) {
metadata.insert(key.to_string(), crate::llm::vm_value_to_json(value));
}
}
}
if let Some(error) = error {
metadata.insert("error".to_string(), serde_json::json!(error));
}
let event = crate::llm::helpers::transcript_event(
"skill.loaded",
"system",
"internal",
&match error {
Some(error) => format!("Skill load blocked for {skill_id}: {error}"),
None => format!("Loaded skill {skill_id}"),
},
Some(serde_json::Value::Object(metadata)),
);
let _ = crate::agent_sessions::append_event(session_id, event);
}
pub fn vm_error(message: impl Into<String>) -> VmError {
VmError::Thrown(VmValue::String(Rc::from(message.into())))
}
pub fn tool_rejected_error(message: impl Into<String>) -> VmError {
VmError::CategorizedError {
message: message.into(),
category: ErrorCategory::ToolRejected,
}
}