From 5620bab61cf3fc08cdcd4cf7461b0733252664d5 Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Fri, 14 Aug 2026 01:36:54 +0000 Subject: [PATCH] Add bounded skill model delegation instructions (#38475) ## What changed - Add `SkillModelDelegationInstruction` for skills that request Luna while running on Sol or Terra. - Resolve Luna only when it is available in the current provider namespace. - Bound and validate model identifiers, skill names, and the rendered instruction before exposing it to callers. ## Testing - Cover supported parent models, provider namespace matching, unavailable or unsafe targets, instruction rendering, and size limits. GitOrigin-RevId: 188a88c36ca32689cbf1e53465283dff4137cb15 --- codex-rs/skills/src/lib.rs | 1 + codex-rs/skills/src/model_delegation.rs | 72 ++++++++ codex-rs/skills/src/model_delegation_tests.rs | 165 ++++++++++++++++++ 3 files changed, 238 insertions(+) diff --git a/codex-rs/skills/src/lib.rs b/codex-rs/skills/src/lib.rs index f2e491b66e..4804a96c5a 100644 --- a/codex-rs/skills/src/lib.rs +++ b/codex-rs/skills/src/lib.rs @@ -38,6 +38,7 @@ pub use model::SkillMetadata; pub use model::SkillPolicy; pub use model::SkillToolDependency; pub use model_delegation::SkillModel; +pub use model_delegation::SkillModelDelegationInstruction; pub use name_counts::build_skill_name_counts; pub use parser::ParsedSkillFrontmatter; pub use parser::SkillParseError; diff --git a/codex-rs/skills/src/model_delegation.rs b/codex-rs/skills/src/model_delegation.rs index 059f058dcb..d6f70f3e7e 100644 --- a/codex-rs/skills/src/model_delegation.rs +++ b/codex-rs/skills/src/model_delegation.rs @@ -1,5 +1,11 @@ use serde::Deserialize; +const LUNA_MODEL: &str = "gpt-5.6-luna"; +const TERRA_MODEL: &str = "gpt-5.6-terra"; +const SOL_MODEL: &str = "gpt-5.6-sol"; +const MAX_TARGET_MODEL_BYTES: usize = 128; +const MAX_DELEGATION_INSTRUCTION_BYTES: usize = 2_048; + /// Model requested for work governed by a skill. #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "lowercase")] @@ -7,6 +13,72 @@ pub enum SkillModel { Luna, } +/// Bounded instructions for delegating skill-governed work to a cheaper model. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SkillModelDelegationInstruction(String); + +impl SkillModelDelegationInstruction { + /// Builds bounded instructions only when the indexed skill requests an available lower-tier model. + pub fn from_skill_model( + skill_model: SkillModel, + skill_name: &str, + current_model: &str, + available_models: &[String], + ) -> Option { + if skill_name + .chars() + .any(|character| matches!(character, '`' | '<' | '>')) + { + return None; + } + + let provider_prefix = [SOL_MODEL, TERRA_MODEL] + .into_iter() + .find_map(|parent_model| { + current_model.strip_suffix(parent_model).filter(|prefix| { + prefix.is_empty() || prefix.ends_with('.') || prefix.ends_with('/') + }) + })?; + let target_model = format!("{provider_prefix}{LUNA_MODEL}"); + let target_model = available_models + .iter() + .find(|model| model.as_str() == target_model && is_safe_model_identifier(model))?; + let skill_model = match skill_model { + SkillModel::Luna => "luna", + }; + let instruction = format!( + "\n\ +For this invocation only, skill `{skill_name}` requests `model: {skill_model}`. \ +If the user prohibits delegation or subagents, or the work depends on an image or audio \ +attachment, work locally. Otherwise, delegate only self-contained text-based skill work \ +and use `spawn_agent` \ +exactly once. Set `model` to `{target_model}`, set `fork_turns` to `\"none\"`, and choose a \ +unique `task_name`. Omit `agent_type` and `reasoning_effort`.\n\ +Give the child only this skill's work and necessary context; never include this block and tell \ +it not to spawn agents. Delegate the whole request only if the skill fully covers it; otherwise, \ +retain ownership of the remaining work and final answer. Wait for the result. If spawning \ +fails, continue locally. If waiting fails, retry waiting without duplicating the \ +child's work.\n\ +Ignore this instruction in child agents and on later turns.\n\ +" + ); + + (instruction.len() <= MAX_DELEGATION_INSTRUCTION_BYTES).then_some(Self(instruction)) + } + + /// Returns the bounded model-visible delegation instructions. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +fn is_safe_model_identifier(model: &str) -> bool { + model.len() <= MAX_TARGET_MODEL_BYTES + && model.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '.' | '/' | '_' | '-') + }) +} + #[cfg(test)] #[path = "model_delegation_tests.rs"] mod tests; diff --git a/codex-rs/skills/src/model_delegation_tests.rs b/codex-rs/skills/src/model_delegation_tests.rs index 85a2b770e6..d421510e8d 100644 --- a/codex-rs/skills/src/model_delegation_tests.rs +++ b/codex-rs/skills/src/model_delegation_tests.rs @@ -1,6 +1,8 @@ use pretty_assertions::assert_eq; +use super::MAX_DELEGATION_INSTRUCTION_BYTES; use super::SkillModel; +use super::SkillModelDelegationInstruction; use crate::ParsedSkillFrontmatter; use crate::parse_skill_frontmatter_metadata; @@ -57,3 +59,166 @@ fn reuses_existing_frontmatter_scalar_repair() { Some(SkillModel::Luna) ); } + +#[test] +fn delegates_only_from_supported_parent_models() { + let available_models = available_models(); + for (current_model, should_delegate) in [ + ("gpt-5.6-sol", true), + ("gpt-5.6-terra", true), + ("gpt-5.6-luna", false), + ("gpt-5.5-codex", false), + ] { + let instruction = SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + "demo", + current_model, + &available_models, + ); + assert_eq!( + instruction.is_some(), + should_delegate, + "current={current_model}" + ); + } +} + +#[test] +fn resolves_luna_within_the_current_provider_namespace() { + for (current_model, target_model, unrelated_model) in [ + ("gpt-5.6-sol", "gpt-5.6-luna", "openai.gpt-5.6-luna"), + ( + "tenant-a/gpt-5.6-sol", + "tenant-a/gpt-5.6-luna", + "tenant-b/gpt-5.6-luna", + ), + ( + "openai.gpt-5.6-terra", + "openai.gpt-5.6-luna", + "other.gpt-5.6-luna", + ), + ] { + let instruction = SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + "demo", + current_model, + &[unrelated_model.to_string(), target_model.to_string()], + ) + .expect("lower-tier model in the same provider namespace should be resolved"); + + assert!( + instruction + .as_str() + .contains(&format!("Set `model` to `{target_model}`")) + ); + } +} + +#[test] +fn rejects_targets_outside_the_current_provider_namespace() { + for (current_model, available_models) in [ + ( + "tenant-a/gpt-5.6-sol", + vec![ + "tenant-b/gpt-5.6-luna".to_string(), + "gpt-5.6-luna".to_string(), + ], + ), + ("gpt-5.6-sol", vec!["tenant-a/gpt-5.6-luna".to_string()]), + ( + "tenant-a/gpt-5.6-sol", + vec!["tenant-a.gpt-5.6-luna".to_string()], + ), + ] { + assert_eq!( + SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + "demo", + current_model, + &available_models, + ), + None, + "current={current_model}, available={available_models:?}" + ); + } +} + +#[test] +fn rejects_unavailable_or_unsafe_models() { + let invalid_identifier = format!("{}/gpt-5.6-luna", "".repeat(32)); + let invalid_current_model = format!("{}/gpt-5.6-sol", "".repeat(32)); + let overlong_identifier = format!("{}.gpt-5.6-luna", "a".repeat(128)); + let overlong_current_model = format!("{}.gpt-5.6-sol", "a".repeat(128)); + for (current_model, available_models) in [ + ( + "custom/gpt-5.6-luna", + vec!["custom/gpt-5.6-luna".to_string()], + ), + ("gpt-5.6-sol", vec!["gpt-5.6-terra".to_string()]), + (invalid_current_model.as_str(), vec![invalid_identifier]), + (overlong_current_model.as_str(), vec![overlong_identifier]), + ] { + assert_eq!( + SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + "demo", + current_model, + &available_models, + ), + None, + "current={current_model}, available={available_models:?}" + ); + } +} + +#[test] +fn renders_bounded_instruction_for_selected_skill_and_model() { + let instruction = SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + "demo", + "gpt-5.6-sol", + &available_models(), + ) + .expect("available lower tier should delegate"); + let rendered = instruction.as_str(); + + assert!(rendered.contains("skill `demo`")); + assert!(rendered.contains("Set `model` to `gpt-5.6-luna`")); + assert!(rendered.contains("image or audio attachment, work locally")); + assert!(rendered.len() <= MAX_DELEGATION_INSTRUCTION_BYTES); +} + +#[test] +fn rejects_skill_names_that_escape_instruction_framing() { + for skill_name in ["unsafe`name", "", ""] { + assert_eq!( + SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + skill_name, + "gpt-5.6-sol", + &available_models(), + ), + None, + "skill_name={skill_name:?}" + ); + } +} + +#[test] +fn rejects_instruction_exceeding_context_bound() { + let skill_name = "x".repeat(MAX_DELEGATION_INSTRUCTION_BYTES); + + assert_eq!( + SkillModelDelegationInstruction::from_skill_model( + SkillModel::Luna, + &skill_name, + "gpt-5.6-sol", + &available_models(), + ), + None + ); +} + +fn available_models() -> Vec { + vec!["gpt-5.6-luna".to_string()] +}