use serde_yaml::Value;
use crate::diagnostic::SchemaDiagnostic;
use crate::dialect::Dialect;
use crate::frontmatter::Frontmatter;
use crate::harness::HarnessId;
use crate::harness_overrides::move_harness_only_keys;
use crate::invocability::find_invocability_field;
use crate::item::DefinitionKind;
use crate::mcp::{ForeignMcpDialect, parse_foreign_mcp_token};
use crate::tool_names::split_tool_name;
use crate::tool_policy;
pub(crate) fn rewrite_tool_entries(
fm: &mut Frontmatter,
key: &str,
rewrite: impl Fn(&str) -> Option<String>,
) -> bool {
let Some(value) = fm.get(key) else {
return false;
};
let mut changed = false;
let mut swap = |entry: &Value| match entry.as_str().and_then(&rewrite) {
Some(renamed) => {
changed = true;
Value::String(renamed)
}
None => entry.clone(),
};
let lifted = match value {
Value::String(_) => swap(value),
Value::Sequence(seq) => Value::Sequence(seq.iter().map(&mut swap).collect()),
Value::Mapping(map) => Value::Mapping(
map.iter()
.map(|(name, entry)| (swap(name), entry.clone()))
.collect(),
),
_ => return false,
};
if changed {
fm.insert(key, lifted);
}
changed
}
fn lift_tool_list_to_policy_map(fm: &mut Frontmatter, restricts: bool) -> bool {
let Some(value) = fm.get("tools") else {
return false;
};
let entries = match value {
Value::String(_) => tool_policy::tool_entries(value),
Value::Sequence(entries) if entries.iter().all(|entry| entry.as_str().is_some()) => {
tool_policy::yaml_str_list(value)
}
Value::Sequence(_)
| Value::Mapping(_)
| Value::Null
| Value::Bool(_)
| Value::Number(_)
| Value::Tagged(_) => return false,
};
let mut policy = serde_yaml::Mapping::new();
if restricts {
policy.insert(Value::String("*".into()), Value::String("deny".into()));
}
for entry in entries {
policy.insert(Value::String(entry), Value::String("allow".into()));
}
fm.insert("tools", Value::Mapping(policy));
true
}
pub(crate) fn lift_foreign_mcp_tokens_in_field(
fm: &mut Frontmatter,
key: &str,
dialect: Dialect,
) -> bool {
let dialect = match dialect {
Dialect::Claude | Dialect::AgentSkills => Some(ForeignMcpDialect::Claude),
Dialect::Cursor => Some(ForeignMcpDialect::Cursor),
Dialect::Codex | Dialect::OpenCode | Dialect::MarsNative => None,
};
dialect.is_some_and(|dialect| {
rewrite_tool_entries(fm, key, |raw| {
parse_foreign_mcp_token(raw, dialect).map(|mcp_ref| mcp_ref.to_canonical())
})
})
}
pub(crate) fn claude_task_to_agent(raw: &str) -> Option<String> {
let (head, payload) = split_tool_name(raw.trim());
matches!(head.trim(), "Task" | "Agent").then(|| format!("agent{payload}"))
}
pub(crate) fn lift_claude_task_entries(fm: &mut Frontmatter, key: &str) -> bool {
rewrite_tool_entries(fm, key, claude_task_to_agent)
}
pub(crate) fn lift_foreign_tools(
fm: &mut Frontmatter,
dialect: Dialect,
native_tools_restrict: bool,
) -> bool {
let mut changed = false;
let mut came_from_native_tools = fm.get("tools").is_some();
if fm.get("tools").is_none() {
for key in ["allowed-tools", "allowed_tools"] {
if let Some(value) = fm.remove(key) {
fm.insert("tools", value);
changed = true;
came_from_native_tools = false;
break;
}
}
} else {
for key in ["allowed-tools", "allowed_tools"] {
if fm.remove(key).is_some() {
changed = true;
}
}
}
changed |= lift_tool_flags(fm);
changed |= lift_tool_list_to_policy_map(fm, native_tools_restrict && came_from_native_tools);
changed |= lift_foreign_mcp_tokens_in_field(fm, "tools", dialect);
if dialect == Dialect::Claude {
changed |= lift_claude_task_entries(fm, "tools");
}
changed
}
fn strip_foreign_keys(fm: &mut Frontmatter, keys: &[&str]) -> bool {
let mut changed = false;
for key in keys {
if fm.remove(key).is_some() {
changed = true;
}
}
changed
}
pub struct Lifted {
pub frontmatter: Frontmatter,
pub changed: bool,
pub changes: Vec<SchemaDiagnostic>,
pub findings: Vec<SchemaDiagnostic>,
}
pub fn lift_frontmatter(
dialect: Dialect,
kind: DefinitionKind,
frontmatter: &Frontmatter,
) -> Lifted {
let mut lifted = frontmatter.clone();
let (mut changes, mut findings) = (Vec::new(), Vec::new());
let mut changed = dialect != Dialect::MarsNative
&& crate::upgrade::lift_mars_only_keys(&mut lifted, kind, &mut changes);
changed |= match dialect {
Dialect::MarsNative => false,
Dialect::Claude | Dialect::AgentSkills => {
crate::claude::translate::lift(kind, &mut lifted, &mut changes, &mut findings)
}
Dialect::Codex => lift_codex(kind, &mut lifted),
Dialect::Cursor => lift_cursor(kind, &mut lifted),
Dialect::OpenCode => lift_opencode(kind, &mut lifted, &mut changes),
};
if dialect != Dialect::MarsNative {
changed |= crate::set_aside::set_aside_rejected_keys(
&mut lifted,
kind,
crate::set_aside::Destination::for_dialect(dialect),
&mut changes,
);
}
Lifted {
frontmatter: lifted,
changed,
changes,
findings,
}
}
fn drop_skill_tool_fields(fm: &mut Frontmatter) -> bool {
let mut changed = false;
for key in crate::skill::tool_keys() {
changed |= fm.remove(key).is_some();
}
changed
}
fn lift_codex(kind: DefinitionKind, fm: &mut Frontmatter) -> bool {
let mut changed = false;
match kind {
DefinitionKind::Skill => {
changed |= strip_foreign_keys(
fm,
&[
"disable-model-invocation",
"allow_implicit_invocation",
"invocation",
"model-invocable",
"model_invocable",
"user-invocable",
"user_invocable",
],
);
changed |= drop_skill_tool_fields(fm);
}
DefinitionKind::Agent => {
changed |= strip_foreign_keys(
fm,
&[
"mode",
"tools",
"disallowedTools",
"disallowed-tools",
"tools_denied",
],
);
}
}
changed
}
fn lift_cursor(kind: DefinitionKind, fm: &mut Frontmatter) -> bool {
match kind {
DefinitionKind::Skill => lift_cursor_rule(fm),
DefinitionKind::Agent => false,
}
}
fn cursor_manual_rule_shape(fm: &Frontmatter) -> bool {
fm.get("alwaysApply").and_then(Value::as_bool) == Some(false)
&& fm.get("description").is_none()
&& fm.get("globs").is_none()
}
fn lift_cursor_rule(fm: &mut Frontmatter) -> bool {
let mut changed = false;
if find_invocability_field(fm, "model-invocable").is_none() {
match fm.get("alwaysApply").and_then(Value::as_bool) {
Some(true) => {
fm.insert("model-invocable", Value::Bool(true));
changed = true;
}
Some(false) if cursor_manual_rule_shape(fm) => {
fm.insert("model-invocable", Value::Bool(false));
changed = true;
}
_ => {}
}
}
if fm.remove("alwaysApply").is_some() {
changed = true;
}
if fm.remove("globs").is_some() {
changed = true;
}
changed |= drop_skill_tool_fields(fm);
changed
}
fn lift_opencode(
kind: DefinitionKind,
fm: &mut Frontmatter,
changes: &mut Vec<SchemaDiagnostic>,
) -> bool {
match kind {
DefinitionKind::Agent => lift_opencode_agent(fm, changes),
DefinitionKind::Skill => {
let mut changed = false;
changed |= strip_foreign_keys(fm, &["mode"]);
changed |= drop_skill_tool_fields(fm);
changed
}
}
}
const OPENCODE_ONLY_AGENT_KEYS: &[&str] = &[
"permission",
"temperature",
"top_p",
"steps",
"maxSteps",
"options",
"hidden",
"disable",
"color",
"prompt",
];
pub(crate) fn is_opencode_agent(fm: &Frontmatter) -> bool {
matches!(fm.get("permission"), Some(Value::Mapping(_))) || tool_flags(fm).is_some()
}
fn lift_opencode_agent(fm: &mut Frontmatter, changes: &mut Vec<SchemaDiagnostic>) -> bool {
let mut changed = false;
let mut denied = Vec::new();
for key in ["disallowedTools", "disallowed-tools", "disallowed_tools"] {
if let Some(value) = fm.remove(key) {
changed = true;
denied.extend(tool_policy::tool_entries(&value));
}
}
changed |= lift_tool_flags(fm);
if !denied.is_empty() {
let mut tools = match fm.get("tools") {
Some(Value::Mapping(existing)) => existing.clone(),
_ => serde_yaml::Mapping::new(),
};
for name in denied {
tools.insert(Value::String(name), Value::String("deny".into()));
}
fm.insert("tools", Value::Mapping(tools));
}
changed |= move_harness_only_keys(
fm,
DefinitionKind::Agent,
HarnessId::OpenCode,
OPENCODE_ONLY_AGENT_KEYS,
changes,
);
changed
}
fn tool_flags(fm: &Frontmatter) -> Option<&serde_yaml::Mapping> {
match fm.get("tools") {
Some(Value::Mapping(tools)) if tools.values().any(Value::is_bool) => Some(tools),
_ => None,
}
}
fn lift_tool_flags(fm: &mut Frontmatter) -> bool {
let Some(tools) = tool_flags(fm) else {
return false;
};
let policy = tools
.iter()
.map(|(name, flag)| {
let action = match flag {
Value::Bool(true) => Value::String("allow".into()),
Value::Bool(false) => Value::String("deny".into()),
other => other.clone(),
};
(name.clone(), action)
})
.collect();
fm.insert("tools", Value::Mapping(policy));
true
}