use starlark::environment::{GlobalsBuilder, LibraryExtension};
use starlark::eval::Evaluator;
use starlark::starlark_module;
use starlark::values::Value;
use starlark::values::dict::{AllocDict, DictRef};
use starlark::values::none::NoneType;
use crate::eval_context::{EvalContext, PolicyRegistration, SettingsValue, ShadowedRule};
pub fn clash_globals() -> starlark::environment::Globals {
let mut builder = GlobalsBuilder::standard();
LibraryExtension::StructType.add(&mut builder);
register_globals(&mut builder);
builder.build()
}
fn caller_source_location(eval: &Evaluator) -> Option<String> {
let stack = eval.call_stack();
for frame in &stack.frames {
if let Some(loc) = &frame.location {
let filename = loc.file.filename();
if !filename.starts_with("@clash//") {
return Some(loc.to_string());
}
}
}
None
}
fn deep_merge<'v>(
left: Value<'v>,
right: Value<'v>,
path: &[String],
ctx: Option<&EvalContext>,
heap: &'v starlark::values::Heap,
) -> anyhow::Result<Value<'v>> {
let left_dict = DictRef::from_value(left)
.ok_or_else(|| anyhow::anyhow!("merge: expected dict, got {}", left.get_type()))?;
let right_dict = DictRef::from_value(right)
.ok_or_else(|| anyhow::anyhow!("merge: expected dict, got {}", right.get_type()))?;
let mut entries: Vec<(Value<'v>, Value<'v>)> = left_dict.iter().collect();
for (rk, rv) in right_dict.iter() {
let existing_idx = entries
.iter()
.position(|(lk, _)| lk.equals(rk).unwrap_or(false));
match existing_idx {
Some(idx) => {
let (_lk, lv) = entries[idx];
let both_dicts =
DictRef::from_value(lv).is_some() && DictRef::from_value(rv).is_some();
if both_dicts {
let key_str = rk.to_repr();
let mut child_path = path.to_vec();
child_path.push(key_str);
let merged = deep_merge(lv, rv, &child_path, ctx, heap)?;
entries[idx].1 = merged;
} else {
if let Some(ctx) = ctx {
let key_str = rk.to_repr();
let mut full_path = path.to_vec();
full_path.push(key_str);
ctx.shadows.borrow_mut().push(ShadowedRule {
path: full_path,
winner: rv.to_repr(),
shadowed: lv.to_repr(),
});
}
entries[idx].1 = rv;
}
}
None => {
entries.push((rk, rv));
}
}
}
Ok(heap.alloc(AllocDict(entries)))
}
#[starlark_module]
fn register_globals(builder: &mut GlobalsBuilder) {
const _ALLOW: &str = "allow";
const _DENY: &str = "deny";
const _ASK: &str = "ask";
const _OS: &str = std::env::consts::OS;
const _ARCH: &str = std::env::consts::ARCH;
fn _from_claude_settings<'v>(
#[starlark(require = named, default = true)] user: bool,
#[starlark(require = named, default = true)] project: bool,
heap: &'v starlark::values::Heap,
) -> anyhow::Result<Value<'v>> {
Ok(crate::settings_compat::from_claude_settings_as_dict(
user, project, heap,
))
}
fn _merge<'v>(
#[starlark(args)] args: &starlark::values::tuple::TupleRef<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<Value<'v>> {
let heap = eval.heap();
let items = args.content();
if items.len() < 2 {
anyhow::bail!(
"merge() requires at least 2 dict arguments, got {}",
items.len()
);
}
for (i, arg) in items.iter().enumerate() {
if DictRef::from_value(*arg).is_none() {
anyhow::bail!("merge() argument {} is not a dict", i + 1);
}
}
let ctx = eval.extra.and_then(|e| e.downcast_ref::<EvalContext>());
let mut result = items[0];
for arg in &items[1..] {
result = deep_merge(result, *arg, &[], ctx, heap)?;
}
Ok(result)
}
fn _register_settings<'v>(
#[starlark(require = named)] default: &str,
#[starlark(require = named, default = starlark::values::none::NoneType)]
default_sandbox: Value<'v>,
#[starlark(require = named, default = starlark::values::none::NoneType)]
on_sandbox_violation: Value<'v>,
#[starlark(require = named, default = starlark::values::none::NoneType)]
harness_defaults: Value<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<NoneType> {
let ctx = eval
.extra
.and_then(|e| e.downcast_ref::<EvalContext>())
.ok_or_else(|| {
anyhow::anyhow!(
"settings() can only be called in a policy file, not in loaded modules"
)
})?;
let ds = if default_sandbox.is_none() {
None
} else {
Some(
default_sandbox
.unpack_str()
.ok_or_else(|| anyhow::anyhow!("default_sandbox must be a string"))?
.to_string(),
)
};
let osv = if on_sandbox_violation.is_none() {
None
} else {
let s = on_sandbox_violation
.unpack_str()
.ok_or_else(|| anyhow::anyhow!("on_sandbox_violation must be a string"))?
.to_string();
match s.as_str() {
"stop" | "workaround" | "smart" => {}
_ => anyhow::bail!(
"on_sandbox_violation must be \"stop\", \"workaround\", or \"smart\", got \"{s}\""
),
}
Some(s)
};
let hd = if harness_defaults.is_none() {
None
} else {
Some(
harness_defaults
.unpack_bool()
.ok_or_else(|| anyhow::anyhow!("harness_defaults must be True or False"))?,
)
};
ctx.register_settings(SettingsValue {
default_effect: default.to_string(),
default_sandbox: ds,
on_sandbox_violation: osv,
harness_defaults: hd,
})?;
Ok(NoneType)
}
fn _policy_impl<'v>(
#[starlark(require = pos)] name: &str,
#[starlark(require = pos)] tree: Value<'v>,
#[starlark(require = named, default = "deny")] default: &str,
#[starlark(require = named, default = starlark::values::none::NoneType)]
default_sandbox: Value<'v>,
#[starlark(require = named, default = starlark::values::none::NoneType)] doc: Value<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<NoneType> {
let _ = doc; let heap = eval.heap();
let ctx = eval
.extra
.and_then(|e| e.downcast_ref::<EvalContext>())
.ok_or_else(|| {
anyhow::anyhow!(
"policy() can only be called in a policy file, not in loaded modules"
)
})?;
if DictRef::from_value(tree).is_none() {
anyhow::bail!(
"policy() requires a dict tree argument, got {}. \
Use the unified shape: policy(\"name\", {{ default(): deny(), mode(\"plan\"): allow(), tool(\"Bash\"): {{...}} }}). \
Run 'clash policy migrate' to convert legacy files.",
tree.get_type()
);
}
let source = caller_source_location(eval);
let (default_override, flat_nodes, sandboxes) =
crate::when::policy_impl(name, tree, default_sandbox, heap, source)?;
let default_effect = default_override.or_else(|| {
if default.is_empty() {
None
} else {
Some(default.to_string())
}
});
ctx.register_policy(PolicyRegistration {
name: name.to_string(),
default_effect,
tree_nodes: flat_nodes,
sandboxes,
})?;
Ok(NoneType)
}
fn _sandbox_impl<'v>(
#[starlark(require = pos)] name: &str,
#[starlark(require = pos)] tree: Value<'v>,
#[starlark(require = named, default = "deny")] default: &str,
#[starlark(require = named, default = starlark::values::none::NoneType)] doc: Value<'v>,
eval: &mut Evaluator<'v, '_, '_>,
) -> anyhow::Result<NoneType> {
let heap = eval.heap();
let ctx = eval
.extra
.and_then(|e| e.downcast_ref::<EvalContext>())
.ok_or_else(|| {
anyhow::anyhow!(
"sandbox() can only be called in a policy file, not in loaded modules"
)
})?;
let doc_str = doc.unpack_str().map(|s| s.to_string());
let source = caller_source_location(eval);
let sb_json = crate::when::sandbox_tree_impl(name, tree, default, doc_str, heap, source)?;
ctx.register_sandbox(name, sb_json)?;
Ok(NoneType)
}
}