use std::cell::RefCell;
use std::sync::Arc;
use crate::value::{VmClosure, VmError, VmValue};
thread_local! {
static TOOL_PRECHECK_STACK: RefCell<Vec<Arc<VmClosure>>> = const { RefCell::new(Vec::new()) };
static TOOL_PRECHECK_DEPTH: RefCell<usize> = const { RefCell::new(0) };
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolPrecheckDenial {
pub model_cue: String,
pub machine_reason: Option<String>,
pub human_summary: Option<String>,
pub retryable: bool,
}
const DEFAULT_PRECHECK_CUE: &str = "command denied by policy";
pub fn push_tool_precheck(precheck: Arc<VmClosure>) {
TOOL_PRECHECK_STACK.with(|stack| stack.borrow_mut().push(precheck));
}
pub fn pop_tool_precheck() {
TOOL_PRECHECK_STACK.with(|stack| {
stack.borrow_mut().pop();
});
}
pub fn clear_tool_prechecks() {
TOOL_PRECHECK_STACK.with(|stack| stack.borrow_mut().clear());
TOOL_PRECHECK_DEPTH.with(|depth| *depth.borrow_mut() = 0);
}
pub fn current_tool_precheck() -> Option<Arc<VmClosure>> {
TOOL_PRECHECK_STACK.with(|stack| stack.borrow().last().cloned())
}
pub fn tool_precheck_active() -> bool {
TOOL_PRECHECK_STACK.with(|stack| !stack.borrow().is_empty())
}
pub(crate) fn swap_tool_precheck_stack(next: Vec<Arc<VmClosure>>) -> Vec<Arc<VmClosure>> {
TOOL_PRECHECK_STACK.with(|stack| std::mem::replace(&mut *stack.borrow_mut(), next))
}
pub(crate) fn swap_tool_precheck_depth(next: usize) -> usize {
TOOL_PRECHECK_DEPTH.with(|depth| std::mem::replace(&mut *depth.borrow_mut(), next))
}
pub fn parse_tool_precheck_value(
value: Option<&VmValue>,
label: &str,
) -> Result<Option<Arc<VmClosure>>, VmError> {
match value {
None | Some(VmValue::Nil) => Ok(None),
Some(VmValue::Closure(closure)) => Ok(Some(closure.clone())),
Some(other) => Err(VmError::Runtime(format!(
"{label} must be a closure, got {}",
other.type_name()
))),
}
}
struct DepthGuard;
impl Drop for DepthGuard {
fn drop(&mut self) {
TOOL_PRECHECK_DEPTH.with(|depth| {
let mut depth = depth.borrow_mut();
*depth = depth.saturating_sub(1);
});
}
}
pub async fn run_tool_precheck(
ctx: Option<&crate::vm::AsyncBuiltinCtx>,
tool_name: &str,
tool_args: &serde_json::Value,
session_id: &str,
) -> Result<Option<ToolPrecheckDenial>, VmError> {
let Some(precheck) = current_tool_precheck() else {
return Ok(None);
};
if TOOL_PRECHECK_DEPTH.with(|depth| *depth.borrow()) > 0 {
return Ok(None);
}
let Some(mut vm) = ctx.map(crate::vm::AsyncBuiltinCtx::child_vm) else {
return Ok(None);
};
let payload = serde_json::json!({
"tool": tool_name,
"arguments": tool_args,
"session_id": session_id,
});
let arg = crate::stdlib::json_to_vm_value(&payload);
TOOL_PRECHECK_DEPTH.with(|depth| *depth.borrow_mut() += 1);
let _guard = DepthGuard;
let verdict = vm.call_closure_pub(&precheck, &[arg]).await?;
Ok(parse_precheck_verdict(verdict))
}
fn parse_precheck_verdict(value: VmValue) -> Option<ToolPrecheckDenial> {
match value {
VmValue::Nil | VmValue::Bool(true) => None,
VmValue::Bool(false) => Some(default_denial()),
VmValue::String(text) => Some(ToolPrecheckDenial {
model_cue: text.to_string(),
machine_reason: None,
human_summary: None,
retryable: false,
}),
VmValue::Dict(map) => {
if truthy(map.get("allow"))
|| map.get("decision").map(|v| v.display()).as_deref() == Some("allow")
{
return None;
}
match map.get("deny") {
Some(VmValue::String(text)) => Some(ToolPrecheckDenial {
model_cue: text.to_string(),
machine_reason: None,
human_summary: None,
retryable: false,
}),
Some(VmValue::Dict(inner)) => Some(denial_from_descriptor(inner)),
Some(VmValue::Bool(true)) => Some(denial_from_descriptor(&map)),
_ => {
if map.get("model_cue").is_some() || map.get("reason").is_some() {
Some(denial_from_descriptor(&map))
} else {
None
}
}
}
}
_ => None,
}
}
fn denial_from_descriptor(map: &crate::value::DictMap) -> ToolPrecheckDenial {
let model_cue = string_field(map, &["model_cue", "reason", "message"])
.unwrap_or_else(|| DEFAULT_PRECHECK_CUE.to_string());
ToolPrecheckDenial {
model_cue,
machine_reason: string_field(map, &["machine_reason"]),
human_summary: string_field(map, &["human_summary"]),
retryable: truthy(map.get("retryable")),
}
}
fn default_denial() -> ToolPrecheckDenial {
ToolPrecheckDenial {
model_cue: DEFAULT_PRECHECK_CUE.to_string(),
machine_reason: None,
human_summary: None,
retryable: false,
}
}
fn string_field(map: &crate::value::DictMap, keys: &[&str]) -> Option<String> {
keys.iter().find_map(|key| match map.get(*key) {
Some(VmValue::String(text)) if !text.is_empty() => Some(text.to_string()),
_ => None,
})
}
fn truthy(value: Option<&VmValue>) -> bool {
matches!(value, Some(VmValue::Bool(true)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nil_and_true_allow() {
assert_eq!(parse_precheck_verdict(VmValue::Nil), None);
assert_eq!(parse_precheck_verdict(VmValue::Bool(true)), None);
}
#[test]
fn bare_string_is_model_cue() {
let denial = parse_precheck_verdict(VmValue::string("blocked: use look")).unwrap();
assert_eq!(denial.model_cue, "blocked: use look");
assert_eq!(denial.machine_reason, None);
assert_eq!(denial.human_summary, None);
assert!(!denial.retryable);
}
#[test]
fn descriptor_splits_audiences() {
let mut map = crate::value::DictMap::new();
map.insert(crate::value::intern_key("deny"), VmValue::Bool(true));
map.insert(
crate::value::intern_key("model_cue"),
VmValue::string("command denied by policy pattern 'ls *'; use `look`"),
);
map.insert(
crate::value::intern_key("machine_reason"),
VmValue::string("pattern 'ls *'"),
);
map.insert(
crate::value::intern_key("human_summary"),
VmValue::string("Blocked by command policy."),
);
let denial = parse_precheck_verdict(VmValue::dict(map)).unwrap();
assert_eq!(denial.machine_reason.as_deref(), Some("pattern 'ls *'"));
assert_eq!(
denial.human_summary.as_deref(),
Some("Blocked by command policy.")
);
assert!(denial.model_cue.contains("use `look`"));
}
#[test]
fn explicit_allow_dict_is_none() {
let mut map = crate::value::DictMap::new();
map.insert(crate::value::intern_key("allow"), VmValue::Bool(true));
assert_eq!(parse_precheck_verdict(VmValue::dict(map)), None);
}
#[test]
fn bare_dict_without_cue_fails_open() {
let mut map = crate::value::DictMap::new();
map.insert(crate::value::intern_key("unrelated"), VmValue::Int(1));
assert_eq!(parse_precheck_verdict(VmValue::dict(map)), None);
}
}