Move trusted skill evidence into the Guardian context registry (#43599)

## What changed

Move trusted skill rendering into `codex-guardian-context` as `TrustedSkills` and collect it through an async-only context section. Pass the collected section to the sampler, preserving the separate developer message and bounded JSON rendering. Omit skill paths from `Debug` output, exposing only their count.

## Testing

Move rendering tests alongside the shared type, covering multiple paths and escaped UTF-8 within the size budget. Extend registry tests to cover async-only inclusion, omission for empty input, section ordering, and path redaction in debug output.

GitOrigin-RevId: c0778067f19d31e693ca954aee1a7d7be8f9739a
This commit is contained in:
felixxia-oai
2026-09-07 21:38:25 +00:00
committed by copyberry
parent 2554239561
commit f5331dc237
14 changed files with 222 additions and 129 deletions
+4 -1
View File
@@ -248,7 +248,9 @@ pub(crate) async fn build_guardian_prompt_items_with_parent_turn(
push_text(text);
}
}
ContextSection::PreviousReviews(_) | ContextSection::TrustedTool(_) => {
ContextSection::PreviousReviews(_)
| ContextSection::TrustedTool(_)
| ContextSection::TrustedSkills(_) => {
unreachable!("trusted review and tool sections are async-only")
}
ContextSection::ConversationTranscript { .. } => {}
@@ -485,6 +487,7 @@ pub(super) fn collect_guardian_context(
permissions,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
}
@@ -580,6 +580,7 @@ impl GuardianV2Extension {
planned_action: Some(&action_section),
previous_reviews: Some(&reviews),
trusted_tool: trusted_tool_context.as_ref(),
trusted_skill_paths: &trusted_skill_paths,
})
});
let transcript = match transcript {
@@ -611,8 +612,10 @@ impl GuardianV2Extension {
let mut classification_input = Vec::new();
let mut trusted_review_evidence = None;
let mut trusted_tool_context = None;
let mut trusted_skills = None;
for section in transcript.sections {
match section {
ContextSection::TrustedSkills(skills) => trusted_skills = Some(skills),
ContextSection::TrustedTool(tool) => trusted_tool_context = Some(tool),
ContextSection::PreviousReviews(reviews) => {
trusted_review_evidence = Some(reviews)
@@ -668,7 +671,7 @@ impl GuardianV2Extension {
instructions,
trusted_review_evidence,
trusted_tool_context,
trusted_skill_paths,
trusted_skills,
input: classification_input,
images,
parent_compaction,
@@ -39,7 +39,7 @@ use thiserror::Error;
use tokio::sync::oneshot;
use uuid::Uuid;
use super::trusted_skills::GuardianTrustedSkillsFragment;
use codex_guardian_context::TrustedSkills;
use codex_guardian_context::TrustedTool;
pub(crate) const MODEL: &str = "gpt-5.6-luna";
@@ -90,7 +90,7 @@ pub struct LunaSamplingRequest {
/// Host-attested metadata for the current home-owned MCP tool or connector.
pub trusted_tool_context: Option<TrustedTool>,
/// Host-verified paths of user-owned skills invoked during this turn.
pub trusted_skill_paths: Vec<String>,
pub trusted_skills: Option<TrustedSkills>,
/// Ordered untrusted input entries that the model should classify.
pub input: Vec<String>,
/// Optional bounded screenshots accompanying the transcript.
@@ -293,12 +293,8 @@ impl LunaSampler {
if let Some(fragment) = request.trusted_tool_context {
input.push(ContextualUserFragment::into(fragment));
}
if !request.trusted_skill_paths.is_empty() {
input.push(ContextualUserFragment::into(
GuardianTrustedSkillsFragment {
paths: request.trusted_skill_paths,
},
));
if let Some(skills) = request.trusted_skills {
input.push(ContextualUserFragment::into(skills));
}
input.push(ResponseItem::Message {
id: None,
@@ -222,7 +222,7 @@ pub(super) fn sample_request(parent_turn_id: &str) -> LunaSamplingRequest {
instructions: "Return high for high risk or low for low risk.".to_owned(),
trusted_review_evidence: None,
trusted_tool_context: None,
trusted_skill_paths: Vec::new(),
trusted_skills: None,
input: vec!["The user requested a README summary.".to_owned()],
images: Vec::new(),
parent_compaction: None,
@@ -480,7 +480,7 @@ async fn preconnected_sampler_reuses_authenticated_websocket_for_classifications
instructions: "Return high for high risk or low for low risk.".to_owned(),
trusted_review_evidence: None,
trusted_tool_context: None,
trusted_skill_paths: Vec::new(),
trusted_skills: None,
input: vec![
"The user requested a README summary.".to_owned(),
"The assistant inspected README.md.".to_owned(),
@@ -515,7 +515,7 @@ async fn preconnected_sampler_reuses_authenticated_websocket_for_classifications
instructions: "Return high for high risk or low for low risk.".to_owned(),
trusted_review_evidence: None,
trusted_tool_context: None,
trusted_skill_paths: Vec::new(),
trusted_skills: None,
input: vec!["The user requested a source review.".to_owned()],
images: Vec::new(),
parent_compaction: None,
@@ -693,7 +693,7 @@ async fn sampler_returns_classification_token_before_terminal_response_events()
instructions: "Return high for high risk or low for low risk.".to_owned(),
trusted_review_evidence: None,
trusted_tool_context: None,
trusted_skill_paths: Vec::new(),
trusted_skills: None,
input: vec!["The user requested a README summary.".to_owned()],
images: Vec::new(),
parent_compaction: None,
@@ -898,7 +898,9 @@ async fn sampler_retries_expired_websockets_on_another_warm_connection() -> Resu
request.trusted_review_evidence = Some(PreviousReviews::try_from_fragments(vec![
"trusted review".to_owned(),
])?);
request.trusted_skill_paths = vec!["/skills/review/SKILL.md".to_owned()];
request.trusted_skills = Some(codex_guardian_context::TrustedSkills {
paths: vec!["/skills/review/SKILL.md".to_owned()],
});
request.root_turn_id = Some("root-turn".to_owned());
let output = sampler.sample(request).await?;
@@ -64,6 +64,7 @@ pub(crate) struct ContextInput<'a> {
pub(crate) planned_action: Option<&'a PlannedAction>,
pub(crate) previous_reviews: Option<&'a PreviousReviews>,
pub(crate) trusted_tool: Option<&'a TrustedTool>,
pub(crate) trusted_skill_paths: &'a [String],
}
pub(crate) struct RenderedContext {
@@ -192,6 +193,7 @@ impl TranscriptConfig {
planned_action,
previous_reviews,
trusted_tool,
trusted_skill_paths,
} = input;
let history = SnapshotHistory(history);
let retention = TranscriptRetentionConfig {
@@ -221,6 +223,7 @@ impl TranscriptConfig {
permissions: None,
previous_reviews,
trusted_tool,
trusted_skill_paths,
})?;
let mut truncations = Vec::new();
let sections = context
@@ -243,6 +246,7 @@ impl TranscriptConfig {
ContextSection::PermissionContext { items } => {
ContextSection::PermissionContext { items }
}
ContextSection::TrustedSkills(skills) => ContextSection::TrustedSkills(skills),
ContextSection::TrustedTool(tool) => ContextSection::TrustedTool(tool),
ContextSection::PreviousReviews(reviews) => {
ContextSection::PreviousReviews(reviews)
@@ -109,6 +109,7 @@ fn transcript_keeps_conversation_and_configured_sources() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -134,6 +135,7 @@ fn transcript_keeps_conversation_and_configured_sources() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("compose authorization and transcript");
assert_eq!(
@@ -165,6 +167,7 @@ fn transcript_keeps_conversation_and_configured_sources() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -191,6 +194,7 @@ fn transcript_keeps_conversation_and_configured_sources() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -242,6 +246,7 @@ fn transcript_truncates_oversized_entries_without_splitting_characters() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript");
let transcript = rendered.transcript_entries();
@@ -308,6 +313,7 @@ fn transcript_preserves_first_and_latest_user_messages_and_recent_history() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -372,6 +378,7 @@ fn transcript_preserves_user_restrictions_before_final_assistant_messages() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -410,6 +417,7 @@ fn transcript_preserves_recent_tool_evidence_when_protected_messages_fill_entry_
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript");
@@ -474,6 +482,7 @@ fn transcript_reserves_five_recent_tool_entries_from_protected_messages() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -528,6 +537,7 @@ fn rejected_commentary_does_not_evict_retained_message_evidence() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -567,6 +577,7 @@ fn transcript_evicts_protected_messages_in_cacheable_chunks() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries()
@@ -636,6 +647,7 @@ fn transcript_preserves_latest_final_when_reserved_tools_fill_entry_window() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -707,6 +719,7 @@ fn transcript_does_not_protect_legacy_inter_agent_instructions() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -752,6 +765,7 @@ fn transcript_reserves_separate_budget_for_recent_tool_evidence() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -812,6 +826,7 @@ fn transcript_reserves_separate_budget_for_recent_tool_evidence() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -874,6 +889,7 @@ fn transcript_preserves_newest_manual_approval_when_message_budget_overflows() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -932,6 +948,7 @@ fn rejected_message_does_not_evict_retained_tool_entries() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -975,6 +992,7 @@ fn transcript_evicts_non_user_entries_in_cacheable_chunks() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries()
@@ -1046,6 +1064,7 @@ fn transcript_truncates_tool_results_using_standard_budget() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -1099,6 +1118,7 @@ fn transcript_preserves_outputs_with_call_ids_or_explicit_names() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries(),
@@ -1126,6 +1146,7 @@ fn transcript_preserves_outputs_with_call_ids_or_explicit_names() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries(),
@@ -1171,6 +1192,7 @@ fn configured_reasoning_counts_against_message_budget() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -1238,6 +1260,7 @@ fn transcript_keeps_only_manual_approval_developer_messages() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -1323,6 +1346,7 @@ fn transcript_omits_media_payloads_and_keeps_readable_content() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -1390,6 +1414,7 @@ fn transcript_omits_encrypted_messages_arguments_and_tool_outputs() {
planned_action: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("collect transcript")
.transcript_entries();
@@ -5,70 +5,11 @@ use std::path::Path;
use std::path::PathBuf;
use codex_core::config::Config;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ContextualUserFragment;
use codex_protocol::protocol::TruncationPolicy;
use dirs::home_dir;
use serde_json::json;
const MAX_TRUSTED_SKILLS: usize = 16;
const MAX_TRUSTED_SKILL_PATH_BYTES: usize = 512;
const MAX_TRUSTED_SKILL_PATHS_BYTES: usize = 2_048;
const MAX_TRUSTED_SKILL_TOKENS: usize = 768;
const TRUSTED_SKILLS_PREFIX: &str = "Codex-verified invoked user-owned skill paths:\n";
/// Host-verified user-owned skill paths for the current Guardian classification.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct GuardianTrustedSkillsFragment {
pub(crate) paths: Vec<String>,
}
impl ContextualUserFragment for GuardianTrustedSkillsFragment {
fn content_kind(&self) -> ContentItemKind {
ContentItemKind("guardian.trusted_skills".to_owned())
}
fn role(&self) -> &'static str {
"developer"
}
fn requires_separate_message(&self) -> bool {
true
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("", "")
}
fn body(&self) -> String {
let mut paths = String::from("[");
let max_path_bytes = TruncationPolicy::Tokens(MAX_TRUSTED_SKILL_TOKENS)
.byte_budget()
.saturating_sub(TRUSTED_SKILLS_PREFIX.len());
for path in &self.paths {
let separator = if paths.ends_with('[') { "" } else { "," };
let encoded_path = json!(path).to_string();
if paths
.len()
.saturating_add(separator.len())
.saturating_add(encoded_path.len())
.saturating_add(1)
> max_path_bytes
{
continue;
}
paths.push_str(separator);
paths.push_str(&encoded_path);
}
paths.push(']');
format!("{TRUSTED_SKILLS_PREFIX}{paths}")
}
}
/// Host-owned roots from which invoked skill paths can be verified.
pub(crate) struct TrustedSkillRoots {
roots: Vec<PathBuf>,
@@ -1,68 +1,13 @@
use super::GuardianTrustedSkillsFragment;
use super::MAX_TRUSTED_SKILL_PATHS_BYTES;
use super::MAX_TRUSTED_SKILL_TOKENS;
use super::MAX_TRUSTED_SKILLS;
use super::TRUSTED_SKILLS_PREFIX;
use super::TrustedSkillInvocations;
use super::TrustedSkillRoots;
use anyhow::Result;
use codex_extension_api::ContextualUserFragment;
use codex_protocol::protocol::TruncationPolicy;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex::test_codex;
use pretty_assertions::assert_eq;
fn rendered_paths(paths: Vec<String>) -> Vec<String> {
let rendered = GuardianTrustedSkillsFragment { paths }.render();
assert!(rendered.len() <= TruncationPolicy::Tokens(MAX_TRUSTED_SKILL_TOKENS).byte_budget());
serde_json::from_str(
rendered
.strip_prefix(TRUSTED_SKILLS_PREFIX)
.expect("trusted skill context should contain JSON evidence"),
)
.expect("trusted skill evidence should remain valid JSON")
}
#[test]
fn renders_verified_skill_paths() {
assert_eq!(
rendered_paths(vec!["/home/user/.codex/skills/demo/SKILL.md".to_owned()]),
vec!["/home/user/.codex/skills/demo/SKILL.md"],
);
}
#[test]
fn bounds_escaped_skill_paths_without_corrupting_json_or_utf8() {
let paths = (0..MAX_TRUSTED_SKILLS)
.map(|index| {
format!(
"/home/user/.codex/skills/{index:03}/{}SKILL.md",
"\u{0001}é".repeat(80)
)
})
.collect::<Vec<_>>();
let retained = rendered_paths(paths.clone());
assert!(!retained.is_empty());
assert!(retained.len() < paths.len());
assert!(retained.iter().all(|path| paths.contains(path)));
}
#[test]
fn preserves_multiple_invoked_skill_paths() {
assert_eq!(
rendered_paths(vec![
"/home/user/.codex/skills/first/SKILL.md".to_owned(),
"/home/user/.codex/skills/second/SKILL.md".to_owned(),
]),
vec![
"/home/user/.codex/skills/first/SKILL.md",
"/home/user/.codex/skills/second/SKILL.md",
],
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn trusts_only_user_owned_skill_roots() -> Result<()> {
skip_if_no_network!(Ok(()));
+5
View File
@@ -51,7 +51,9 @@ mod authorization;
mod entry;
mod history;
mod permissions;
mod trusted_skills;
mod trusted_tool;
pub use trusted_skills::TrustedSkills;
pub use trusted_tool::TrustedTool;
mod reviews;
pub use reviews::MAX_PREVIOUS_REVIEWS;
@@ -118,6 +120,8 @@ pub struct SectionInput<'a> {
pub previous_reviews: Option<&'a PreviousReviews>,
/// Metadata verified by the host for the exact action being classified.
pub trusted_tool: Option<&'a TrustedTool>,
/// Current-turn and delegated skill paths verified and bounded by the host.
pub trusted_skill_paths: &'a [String],
}
/// Supplies repeatable, zero-copy access to a host-owned conversation snapshot.
@@ -207,6 +211,7 @@ pub fn default_registry() -> &'static SectionRegistry {
let mut registry = SectionRegistry::default();
registry.register(reviews::PreviousReviewsSection);
registry.register(trusted_tool::TrustedToolSection);
registry.register(trusted_skills::TrustedSkillsSection);
registry.register(RootConversationSection);
registry.register(RetainedUserInstructionsSection);
registry.register(TrustedUserAnswersSection);
@@ -92,6 +92,7 @@ fn registry_collects_target_specific_sections_in_registration_order() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
});
let async_sections = registry.collect(&SectionInput {
target: ContextTarget::Async,
@@ -103,6 +104,7 @@ fn registry_collects_target_specific_sections_in_registration_order() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
});
assert_eq!(
@@ -165,6 +167,7 @@ fn registry_skips_optional_sections_and_stops_on_missing_required_evidence() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
}),
Err(error.clone())
);
@@ -228,6 +231,7 @@ fn reused_registry_preserves_section_identity_and_source_roles() {
permissions: Some(&permissions),
previous_reviews: Some(&reviews),
trusted_tool: Some(&tool),
trusted_skill_paths: &["debug-secret/SKILL.md".into()],
})
.unwrap();
assert!(!format!("{context:?}").contains("debug-secret"));
@@ -260,6 +264,12 @@ fn reused_registry_preserves_section_identity_and_source_roles() {
] });
}
if target == ContextTarget::Async {
expected.insert(
0,
ContextSection::TrustedSkills(super::TrustedSkills {
paths: vec!["debug-secret/SKILL.md".into()],
}),
);
expected.insert(0, ContextSection::TrustedTool(tool.clone()));
expected.insert(0, ContextSection::PreviousReviews(reviews.clone()));
}
@@ -277,6 +287,7 @@ fn reused_registry_preserves_section_identity_and_source_roles() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.unwrap(),
vec![ContextSection::ConversationTranscript { items: Vec::new() }]
+2
View File
@@ -3,6 +3,7 @@
use crate::ConversationTranscriptEntry;
use crate::PlannedAction;
use crate::PreviousReviews;
use crate::TrustedSkills;
use crate::TrustedTool;
/// Ordered evidence with a stable section identity and source-specific content.
@@ -21,5 +22,6 @@ pub enum ContextSection<T = ConversationTranscriptEntry> {
PlannedAction(PlannedAction),
PreviousReviews(PreviousReviews),
TrustedTool(TrustedTool),
TrustedSkills(TrustedSkills),
PermissionContext { items: Vec<String> },
}
@@ -102,6 +102,7 @@ fn registered_transcript_preserves_shared_roles_and_node_repl_tool_attribution()
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("transcript collection should succeed");
@@ -178,6 +179,7 @@ fn excluded_tool_calls_still_attribute_included_results() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("transcript collection should succeed");
@@ -273,6 +275,7 @@ fn outputs_with_call_ids_or_explicit_names_are_retained() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("transcript collection should succeed");
let mut expected = vec![
@@ -322,6 +325,7 @@ fn reused_registry_applies_current_history_sources_and_entry_limits() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("transcript collection should succeed");
assert_eq!(
@@ -358,6 +362,7 @@ fn reused_registry_applies_current_history_sources_and_entry_limits() {
permissions: None,
previous_reviews: None,
trusted_tool: None,
trusted_skill_paths: &[],
})
.expect("transcript collection should succeed");
let mut expected = vec![ConversationTranscriptEntry {
@@ -0,0 +1,95 @@
//! Bounded rendering of host-verified invoked skill paths for async review.
//! Ownership checks, turn storage and delegated evidence selection stay in the host.
use crate::ContextSection;
use crate::SectionContributor;
use crate::SectionError;
use crate::SectionInput;
use crate::SectionScope;
use codex_context_fragments::ContextualUserFragment;
use codex_protocol::models::ContentItemKind;
use codex_protocol::protocol::TruncationPolicy;
use serde_json::json;
const MAX_TRUSTED_SKILL_TOKENS: usize = 768;
const TRUSTED_SKILLS_PREFIX: &str = "Codex-verified invoked user-owned skill paths:\n";
/// Host-verified user-owned skill paths for the current Guardian classification.
#[derive(Clone, PartialEq, Eq)]
pub struct TrustedSkills {
pub paths: Vec<String>,
}
impl ContextualUserFragment for TrustedSkills {
fn content_kind(&self) -> ContentItemKind {
ContentItemKind("guardian.trusted_skills".to_owned())
}
fn role(&self) -> &'static str {
"developer"
}
fn requires_separate_message(&self) -> bool {
true
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
("", "")
}
fn body(&self) -> String {
let mut paths = String::from("[");
let max_path_bytes = TruncationPolicy::Tokens(MAX_TRUSTED_SKILL_TOKENS)
.byte_budget()
.saturating_sub(TRUSTED_SKILLS_PREFIX.len());
for path in &self.paths {
let separator = if paths.ends_with('[') { "" } else { "," };
let encoded_path = json!(path).to_string();
if paths
.len()
.saturating_add(separator.len())
.saturating_add(encoded_path.len())
.saturating_add(1)
> max_path_bytes
{
continue;
}
paths.push_str(separator);
paths.push_str(&encoded_path);
}
paths.push(']');
format!("{TRUSTED_SKILLS_PREFIX}{paths}")
}
}
impl std::fmt::Debug for TrustedSkills {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TrustedSkills")
.field("count", &self.paths.len())
.finish_non_exhaustive()
}
}
pub(crate) struct TrustedSkillsSection;
impl SectionContributor for TrustedSkillsSection {
fn scope(&self) -> SectionScope {
SectionScope::AsyncOnly
}
fn contribute(&self, input: &SectionInput<'_>) -> Result<Option<ContextSection>, SectionError> {
Ok((!input.trusted_skill_paths.is_empty()).then(|| {
ContextSection::TrustedSkills(TrustedSkills {
paths: input.trusted_skill_paths.to_vec(),
})
}))
}
}
#[cfg(test)]
#[path = "trusted_skills_tests.rs"]
mod tests;
@@ -0,0 +1,56 @@
use super::MAX_TRUSTED_SKILL_TOKENS;
use super::TRUSTED_SKILLS_PREFIX;
use super::TrustedSkills;
use codex_context_fragments::ContextualUserFragment;
use codex_protocol::protocol::TruncationPolicy;
use pretty_assertions::assert_eq;
fn rendered_paths(paths: Vec<String>) -> Vec<String> {
let rendered = TrustedSkills { paths }.render();
assert!(rendered.len() <= TruncationPolicy::Tokens(MAX_TRUSTED_SKILL_TOKENS).byte_budget());
serde_json::from_str(
rendered
.strip_prefix(TRUSTED_SKILLS_PREFIX)
.expect("trusted skill context should contain JSON evidence"),
)
.expect("trusted skill evidence should remain valid JSON")
}
#[test]
fn renders_verified_skill_paths() {
assert_eq!(
rendered_paths(vec!["/home/user/.codex/skills/demo/SKILL.md".to_owned()]),
vec!["/home/user/.codex/skills/demo/SKILL.md"],
);
}
#[test]
fn bounds_escaped_skill_paths_without_corrupting_json_or_utf8() {
let paths = (0..16)
.map(|index| {
format!(
"/home/user/.codex/skills/{index:03}/{}SKILL.md",
"\u{0001}é".repeat(80)
)
})
.collect::<Vec<_>>();
let retained = rendered_paths(paths.clone());
assert!(!retained.is_empty());
assert!(retained.len() < paths.len());
assert!(retained.iter().all(|path| paths.contains(path)));
}
#[test]
fn preserves_multiple_invoked_skill_paths() {
assert_eq!(
rendered_paths(vec![
"/home/user/.codex/skills/first/SKILL.md".to_owned(),
"/home/user/.codex/skills/second/SKILL.md".to_owned(),
]),
vec![
"/home/user/.codex/skills/first/SKILL.md",
"/home/user/.codex/skills/second/SKILL.md",
],
);
}