mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Apply elastic entry caps to Guardian classifier evidence (#61346)
## Why Trusted classifier evidence needs to fit configured entry limits while preserving complete records and metadata that reflects only delivered reviews. ## What changed Apply elastic caps before rendering trusted tools, trusted skills, and previous reviews into developer messages. Omit oversized tool attestations and review fragments whole, retain only complete skill paths that fit, and record omissions for the request's omission notice. Keep review IDs aligned with retained fragments and preserve fixed-budget behavior. ## Testing Add coverage for whole-record omissions, retained review IDs, and omission notices. Exercise orchestrator connector evidence with both fixed budgeting and an elastic tool entry cap. GitOrigin-RevId: 729db452026f93b59765a3936730b9b6dddfcf9a
This commit is contained in:
@@ -363,28 +363,8 @@ async fn compacted_review_restores_originals_once_and_persists_the_request_prefi
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
let elastic = (budgeting == ContextBudgeting::Elastic).then(|| {
|
||||
let section = codex_guardian_context::ElasticSection::TrustedSkills;
|
||||
let mut policy = codex_guardian_context::ElasticBudgetConfig::default();
|
||||
policy.sections.insert(
|
||||
section,
|
||||
codex_guardian_context::ElasticSectionLimits::new(
|
||||
/*min_total_section_tokens*/ 1, /*max_section_entry_tokens*/ 1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let minimum = codex_guardian_context::DEFAULT_MAX_INPUT_TOKENS
|
||||
- usize::try_from(policy.total_minimum_tokens()).unwrap()
|
||||
+ 1;
|
||||
policy.sections.insert(
|
||||
section,
|
||||
codex_guardian_context::ElasticSectionLimits::new(
|
||||
minimum, /*max_section_entry_tokens*/ 1,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
policy
|
||||
});
|
||||
let elastic = (budgeting == ContextBudgeting::Elastic)
|
||||
.then(core_test_support::guardian::elastic_budget_config);
|
||||
step.turn.extension_data.insert(RetainedReviewContext {
|
||||
context: context.retained_instructions(),
|
||||
elastic,
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::config::Constrained;
|
||||
use codex_features::Feature;
|
||||
use codex_guardian_context::ContextBudgeting;
|
||||
use codex_guardian_context::DEFAULT_MAX_INPUT_TOKENS;
|
||||
use codex_guardian_context::ElasticSection;
|
||||
use codex_guardian_context::ElasticSectionLimits;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
@@ -27,8 +32,12 @@ use wiremock::matchers::body_partial_json;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[test_case::test_case(ContextBudgeting::Fixed; "fixed")]
|
||||
#[test_case::test_case(ContextBudgeting::Elastic; "elastic_entry_cap")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn host_owned_orchestrator_connector_reaches_guardian_context() -> Result<()> {
|
||||
async fn host_owned_orchestrator_connector_reaches_guardian_context(
|
||||
budgeting: ContextBudgeting,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
@@ -48,14 +57,15 @@ async fn host_owned_orchestrator_connector_reaches_guardian_context() -> Result<
|
||||
responses::ev_completed("done"),
|
||||
]),
|
||||
];
|
||||
Mock::given(method("POST"))
|
||||
let classifier_mock = Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.and(body_partial_json(json!({ "model": "gpt-5.6-luna" })))
|
||||
.respond_with(responses::sse_response(responses::sse(vec![
|
||||
responses::ev_assistant_message("score", "high"),
|
||||
responses::ev_completed("score"),
|
||||
])))
|
||||
.mount(&server)
|
||||
.expect(1..)
|
||||
.mount_as_scoped(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
@@ -98,7 +108,7 @@ async fn host_owned_orchestrator_connector_reaches_guardian_context() -> Result<
|
||||
)
|
||||
.expect("write Guardian configuration");
|
||||
})
|
||||
.with_config(|config| {
|
||||
.with_config(move |config| {
|
||||
config.model_provider.supports_websockets = false;
|
||||
config
|
||||
.features
|
||||
@@ -114,12 +124,34 @@ async fn host_owned_orchestrator_connector_reaches_guardian_context() -> Result<
|
||||
.expect("enable Guardian approval");
|
||||
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
if budgeting == ContextBudgeting::Elastic {
|
||||
const TOOL_ENTRY_CAP: usize = 1;
|
||||
let mut policy = core_test_support::guardian::elastic_budget_config();
|
||||
let limits = |minimum| {
|
||||
ElasticSectionLimits::new(minimum, TOOL_ENTRY_CAP).expect("valid tool limits")
|
||||
};
|
||||
policy.sections.insert(ElasticSection::TrustedTools, limits(TOOL_ENTRY_CAP));
|
||||
let other_minimums = usize::try_from(policy.total_minimum_tokens())
|
||||
.expect("token total fits usize")
|
||||
- TOOL_ENTRY_CAP;
|
||||
policy.sections.insert(
|
||||
ElasticSection::TrustedTools,
|
||||
limits(DEFAULT_MAX_INPUT_TOKENS - other_minimums),
|
||||
);
|
||||
config.guardian_async_elastic_budget = Some(policy);
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
wait_for_mcp_server(&test.codex, "codex_apps").await?;
|
||||
test.submit_text_turn("List today's calendar events.")
|
||||
.await?;
|
||||
const CLASSIFIER_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
tokio::time::timeout(
|
||||
CLASSIFIER_REQUEST_TIMEOUT,
|
||||
classifier_mock.wait_until_satisfied(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(parent_index.load(Ordering::SeqCst), 2);
|
||||
|
||||
let mut requests = server
|
||||
@@ -149,6 +181,19 @@ async fn host_owned_orchestrator_connector_reaches_guardian_context() -> Result<
|
||||
serde_json::from_str::<Value>(evidence)
|
||||
})
|
||||
.collect::<serde_json::Result<Vec<_>>>()?;
|
||||
if budgeting == ContextBudgeting::Elastic {
|
||||
assert!(trusted_messages.is_empty());
|
||||
let classifier = requests
|
||||
.iter()
|
||||
.find(|request| request["model"] == "gpt-5.6-luna")
|
||||
.expect("classifier request");
|
||||
assert!(
|
||||
classifier["input"]
|
||||
.to_string()
|
||||
.contains("<guardian_context_omission>")
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
assert_eq!(
|
||||
trusted_messages,
|
||||
vec![json!({
|
||||
|
||||
@@ -3,7 +3,6 @@ use codex_extension_api::ResponseItem;
|
||||
pub(crate) use codex_features::GuardianV2TranscriptSource as TranscriptSource;
|
||||
use codex_guardian_context::CollectedContext;
|
||||
use codex_guardian_context::ComposedContext;
|
||||
use codex_guardian_context::ContextBudgeting;
|
||||
use codex_guardian_context::ContextPresentation;
|
||||
use codex_guardian_context::ContextProfile;
|
||||
use codex_guardian_context::ContextTarget;
|
||||
@@ -26,8 +25,7 @@ use codex_guardian_context::TranscriptRetentionConfig;
|
||||
use codex_guardian_context::TranscriptSelection;
|
||||
use codex_guardian_context::TrustedTool;
|
||||
use codex_guardian_context::default_registry;
|
||||
use codex_guardian_context::prepare_context;
|
||||
use std::borrow::Cow;
|
||||
use codex_guardian_context::prepare_async_context;
|
||||
|
||||
const DEFAULT_PROFILE: &ContextProfile = &ContextProfile::asynchronous();
|
||||
pub(crate) const MAX_MESSAGE_ENTRY_TOKENS: usize =
|
||||
@@ -185,17 +183,13 @@ impl CollectedTranscript {
|
||||
TranscriptSelection::Full(_) => (0, ContextPresentation::Async),
|
||||
TranscriptSelection::Delta { offset, .. } => (offset, ContextPresentation::AsyncDelta),
|
||||
};
|
||||
let mut context = prepare_context(
|
||||
Cow::Borrowed(&self.context),
|
||||
let mut context = prepare_async_context(
|
||||
&self.context,
|
||||
&self.profile,
|
||||
offset,
|
||||
presentation,
|
||||
if self.elastic.is_some() {
|
||||
ContextBudgeting::Elastic
|
||||
} else {
|
||||
ContextBudgeting::Fixed
|
||||
},
|
||||
reviewer_history,
|
||||
self.elastic.as_ref(),
|
||||
)?;
|
||||
if self.elastic.is_none() {
|
||||
context.retain_new_native_messages(reviewer_history)?;
|
||||
|
||||
@@ -108,6 +108,7 @@ pub use preparation::ContextBudgeting;
|
||||
pub use preparation::ContextProfile;
|
||||
pub use preparation::SyncImagePreparer;
|
||||
pub use preparation::SyncPreparation;
|
||||
pub use preparation::prepare_async_context;
|
||||
pub use preparation::prepare_context;
|
||||
pub use preparation::prepare_sync_context_with_facts;
|
||||
pub use preparation::prepare_sync_delivery;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Caps typed classifier evidence before it becomes trusted developer messages.
|
||||
//! Delivery metadata must describe only the complete records kept for rendering.
|
||||
|
||||
use crate::CollectedContext;
|
||||
use crate::ContextSection;
|
||||
use crate::ElasticBudgetConfig;
|
||||
use crate::ElasticSection;
|
||||
use crate::TruncationObservation;
|
||||
use crate::budget::content_framing_tokens;
|
||||
use crate::budget::content_tokens;
|
||||
use crate::trusted_tool::MAX_TRUSTED_TOOL_CONTEXT_TOKENS;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::input_text;
|
||||
use codex_protocol::protocol::TruncationPolicy;
|
||||
|
||||
pub(super) fn apply(
|
||||
context: &mut CollectedContext,
|
||||
config: &ElasticBudgetConfig,
|
||||
truncations: &mut Vec<TruncationObservation>,
|
||||
) {
|
||||
let cap = |section| config.section_limits(section).max_section_entry_tokens();
|
||||
let mut record_omission = |component, original_bytes| {
|
||||
truncations.push(TruncationObservation {
|
||||
source: None,
|
||||
component,
|
||||
original_bytes,
|
||||
retained_bytes: 0,
|
||||
});
|
||||
};
|
||||
context.sections.retain_mut(|section| match section {
|
||||
ContextSection::TrustedTool(tool) => {
|
||||
let attestation = tool.attestation();
|
||||
let bytes = attestation.len();
|
||||
let rendering_limit =
|
||||
TruncationPolicy::Tokens(MAX_TRUSTED_TOOL_CONTEXT_TOKENS).byte_budget();
|
||||
let keep = bytes <= rendering_limit
|
||||
&& fits(&input_text(attestation), cap(ElasticSection::TrustedTools));
|
||||
if !keep {
|
||||
record_omission("trusted_tool", bytes);
|
||||
}
|
||||
keep
|
||||
}
|
||||
ContextSection::TrustedSkills(skills) => {
|
||||
let maximum = cap(ElasticSection::TrustedSkills);
|
||||
for path in std::mem::take(&mut skills.paths) {
|
||||
let path_bytes = path.len();
|
||||
skills.paths.push(path);
|
||||
let (rendered, count) = skills.render_paths();
|
||||
if count != skills.paths.len() || !fits(&rendered, maximum) {
|
||||
drop(skills.paths.pop());
|
||||
record_omission("trusted_skills", path_bytes);
|
||||
}
|
||||
}
|
||||
!skills.paths.is_empty()
|
||||
}
|
||||
ContextSection::PreviousReviews(reviews) => {
|
||||
let maximum = cap(ElasticSection::PreviousReviews);
|
||||
reviews.retain(|review| {
|
||||
let keep = fits(&input_text(review.fragment.clone()), maximum);
|
||||
if !keep {
|
||||
record_omission("previous_reviews", review.fragment.len());
|
||||
}
|
||||
keep
|
||||
})
|
||||
}
|
||||
_ => true,
|
||||
});
|
||||
}
|
||||
|
||||
fn fits(content: &ContentItem, maximum: usize) -> bool {
|
||||
content_tokens(content).saturating_add(content_framing_tokens(/*item_count*/ 1)) <= maximum
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "async_entries_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,143 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use crate::ContextPresentation;
|
||||
use crate::ContextProfile;
|
||||
use crate::ElasticSectionLimits;
|
||||
use crate::PreviousReview;
|
||||
use crate::PreviousReviews;
|
||||
use crate::RequestBudget;
|
||||
use crate::TrustedSkills;
|
||||
use crate::TrustedTool;
|
||||
use crate::TrustedToolSource;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn classifier_entry_caps_preserve_whole_evidence_and_only_sent_review_ids() {
|
||||
const ENTRY_CAP: usize = 100;
|
||||
const SECTION_MINIMUM: usize = 4_000;
|
||||
const OVERSIZED_BYTES: usize = 500;
|
||||
const NOTICE: &str = "Evidence omitted for this request.";
|
||||
let kept_review = PreviousReview {
|
||||
id: ResponseItemId::from_server("review_kept".to_owned()),
|
||||
fragment: "Approved reading README.md.".to_owned(),
|
||||
};
|
||||
let omitted_review = PreviousReview {
|
||||
id: ResponseItemId::from_server("review_omitted".to_owned()),
|
||||
fragment: format!("Reviewed: {}", "x".repeat(OVERSIZED_BYTES)),
|
||||
};
|
||||
let reviews =
|
||||
PreviousReviews::try_from_fragments(vec![omitted_review, kept_review.clone()]).unwrap();
|
||||
let tool = TrustedTool {
|
||||
server: "files".to_owned(),
|
||||
connector_id: None,
|
||||
source: TrustedToolSource::PluginServiceOrchestrator,
|
||||
};
|
||||
let skill = "/skills/read-only/SKILL.md";
|
||||
let context = CollectedContext {
|
||||
sections: vec![
|
||||
ContextSection::TrustedTool(tool),
|
||||
ContextSection::TrustedSkills(TrustedSkills {
|
||||
paths: vec![
|
||||
format!("/skills/{}", "x".repeat(OVERSIZED_BYTES)),
|
||||
skill.to_owned(),
|
||||
],
|
||||
}),
|
||||
ContextSection::PreviousReviews(reviews),
|
||||
],
|
||||
};
|
||||
let profile = ContextProfile::asynchronous();
|
||||
let fixed = crate::prepare_async_context(
|
||||
&context,
|
||||
&profile,
|
||||
/*entry_number_offset*/ 0,
|
||||
ContextPresentation::Async,
|
||||
&[],
|
||||
/*elastic*/ None,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(fixed.truncations.is_empty());
|
||||
let fixed = fixed.into_annotated_messages();
|
||||
assert_eq!(
|
||||
fixed
|
||||
.iter()
|
||||
.filter(|item| item
|
||||
.metadata
|
||||
.as_ref()
|
||||
.is_some_and(|metadata| metadata.guardian_review_ids.len() > 1,))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
let limits = ElasticSectionLimits::new(SECTION_MINIMUM, ENTRY_CAP).unwrap();
|
||||
let config = crate::budget::tests::complete_minimum_budget(ElasticBudgetConfig {
|
||||
sections: [
|
||||
(
|
||||
ElasticSection::TrustedTools,
|
||||
ElasticSectionLimits::new(SECTION_MINIMUM, /*max_section_entry_tokens*/ 1).unwrap(),
|
||||
),
|
||||
(ElasticSection::TrustedSkills, limits),
|
||||
(ElasticSection::PreviousReviews, limits),
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
});
|
||||
let capped = crate::prepare_async_context(
|
||||
&context,
|
||||
&profile,
|
||||
/*entry_number_offset*/ 0,
|
||||
ContextPresentation::Async,
|
||||
&[],
|
||||
Some(&config),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
capped
|
||||
.truncations
|
||||
.iter()
|
||||
.map(|loss| loss.component)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["trusted_tool", "trusted_skills", "previous_reviews"],
|
||||
);
|
||||
let delivered = capped
|
||||
.prepare_elastic_entries(&config)
|
||||
.unwrap()
|
||||
.prepare_delivery(&[])
|
||||
.unwrap()
|
||||
.enforce_budget(
|
||||
RequestBudget {
|
||||
max_input_tokens: crate::DEFAULT_MAX_INPUT_TOKENS,
|
||||
existing_context_tokens: 0,
|
||||
},
|
||||
NOTICE.to_owned(),
|
||||
)
|
||||
.unwrap()
|
||||
.into_annotated_messages();
|
||||
let expected_review = PreviousReviews::try_from_fragments(vec![kept_review])
|
||||
.unwrap()
|
||||
.into_annotated_message();
|
||||
assert!(delivered.contains(&expected_review));
|
||||
let developers = delivered
|
||||
.iter()
|
||||
.filter_map(|envelope| match &envelope.item {
|
||||
ResponseItem::Message { role, .. } if role == "developer" => Some(&envelope.item),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected_skill = codex_context_fragments::ContextualUserFragment::into(TrustedSkills {
|
||||
paths: vec![skill.to_owned()],
|
||||
});
|
||||
assert_eq!(developers, vec![&expected_review.item, &expected_skill]);
|
||||
assert!(
|
||||
serde_json::to_string(
|
||||
&delivered
|
||||
.iter()
|
||||
.map(|envelope| &envelope.item)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.unwrap()
|
||||
.contains(NOTICE)
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Collection keeps captured sources intact. This owner applies text and image policy;
|
||||
//! delivery checks repeat against current reviewer history after compaction.
|
||||
|
||||
mod async_entries;
|
||||
mod elastic;
|
||||
pub(crate) mod repl;
|
||||
pub(crate) mod root;
|
||||
@@ -13,6 +14,7 @@ use crate::CollectedContext;
|
||||
use crate::ComposedContext;
|
||||
use crate::ContextPresentation;
|
||||
use crate::ContextSection;
|
||||
use crate::ElasticBudgetConfig;
|
||||
use crate::GuardianRootMessage;
|
||||
use crate::RetainedInstructionItem;
|
||||
use crate::SectionError;
|
||||
@@ -58,6 +60,51 @@ pub fn prepare_context(
|
||||
presentation: ContextPresentation<'_>,
|
||||
budgeting: ContextBudgeting,
|
||||
reviewer_history: &[ResponseItemEnvelope],
|
||||
) -> Result<ComposedContext, SectionError> {
|
||||
prepare_context_with_async_caps(
|
||||
context,
|
||||
profile,
|
||||
entry_number_offset,
|
||||
presentation,
|
||||
budgeting,
|
||||
reviewer_history,
|
||||
/*async_elastic*/ None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Prepares the classifier suffix, applying typed async entry caps before rendering.
|
||||
pub fn prepare_async_context(
|
||||
context: &CollectedContext,
|
||||
profile: &ContextProfile,
|
||||
entry_number_offset: usize,
|
||||
presentation: ContextPresentation<'_>,
|
||||
reviewer_history: &[ResponseItemEnvelope],
|
||||
elastic: Option<&ElasticBudgetConfig>,
|
||||
) -> Result<ComposedContext, SectionError> {
|
||||
let budgeting = if elastic.is_some() {
|
||||
ContextBudgeting::Elastic
|
||||
} else {
|
||||
ContextBudgeting::Fixed
|
||||
};
|
||||
prepare_context_with_async_caps(
|
||||
Cow::Borrowed(context),
|
||||
profile,
|
||||
entry_number_offset,
|
||||
presentation,
|
||||
budgeting,
|
||||
reviewer_history,
|
||||
elastic,
|
||||
)
|
||||
}
|
||||
|
||||
fn prepare_context_with_async_caps(
|
||||
context: Cow<'_, CollectedContext>,
|
||||
profile: &ContextProfile,
|
||||
entry_number_offset: usize,
|
||||
presentation: ContextPresentation<'_>,
|
||||
budgeting: ContextBudgeting,
|
||||
reviewer_history: &[ResponseItemEnvelope],
|
||||
async_elastic: Option<&ElasticBudgetConfig>,
|
||||
) -> Result<ComposedContext, SectionError> {
|
||||
let (target, placeholder) = match &presentation {
|
||||
ContextPresentation::SyncFull { .. } => (
|
||||
@@ -113,6 +160,9 @@ pub fn prepare_context(
|
||||
},
|
||||
};
|
||||
context.retain_new_reviews(reviewer_history);
|
||||
if let Some(config) = async_elastic {
|
||||
async_entries::apply(&mut context, config, &mut transcript.truncations);
|
||||
}
|
||||
prepare_sections(&mut context, budgeting)?;
|
||||
context.compose_prepared(presentation, transcript)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ impl std::fmt::Debug for PreviousReviews {
|
||||
}
|
||||
|
||||
impl PreviousReviews {
|
||||
pub(crate) fn retain(&mut self, predicate: impl FnMut(&PreviousReview) -> bool) -> bool {
|
||||
self.fragments.retain(predicate);
|
||||
!self.fragments.is_empty()
|
||||
}
|
||||
|
||||
/// Validates the count and size of complete fragments, including their markers.
|
||||
/// The host remains responsible for provenance and authorization validity.
|
||||
/// Rejects oversized evidence without truncating or dropping any records.
|
||||
@@ -121,13 +126,12 @@ impl CollectedContext {
|
||||
let ContextSection::PreviousReviews(reviews) = section else {
|
||||
return true;
|
||||
};
|
||||
reviews.fragments.retain(|review| {
|
||||
reviews.retain(|review| {
|
||||
!history
|
||||
.iter()
|
||||
.filter_map(|item| item.metadata.as_ref())
|
||||
.any(|metadata| metadata.guardian_review_ids.contains(&review.id))
|
||||
});
|
||||
!reviews.fragments.is_empty()
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,32 @@ pub struct TrustedSkills {
|
||||
impl TrustedSkills {
|
||||
/// Classification attached to this fragment's rendered text.
|
||||
pub const KIND: &str = "guardian.trusted_skills";
|
||||
|
||||
pub(crate) fn render_paths(&self) -> (ContentItem, usize) {
|
||||
let mut paths = String::from("[");
|
||||
let mut count = 0;
|
||||
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);
|
||||
count += 1;
|
||||
}
|
||||
paths.push(']');
|
||||
(input_text(format!("{TRUSTED_SKILLS_PREFIX}{paths}")), count)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for TrustedSkills {
|
||||
@@ -45,27 +71,7 @@ impl ContextualUserFragment for TrustedSkills {
|
||||
}
|
||||
|
||||
fn body(&self) -> ContentItem {
|
||||
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(']');
|
||||
input_text(format!("{TRUSTED_SKILLS_PREFIX}{paths}"))
|
||||
self.render_paths().0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::SectionInput;
|
||||
use crate::SectionScope;
|
||||
use crate::truncate_text as truncate_entry;
|
||||
|
||||
const MAX_TRUSTED_TOOL_CONTEXT_TOKENS: usize = 512;
|
||||
pub(crate) const MAX_TRUSTED_TOOL_CONTEXT_TOKENS: usize = 512;
|
||||
const USER_CONFIGURATION_PREFIX: &str = "Codex verified that this exact MCP tool or connector was declared in \
|
||||
trusted user configuration. ";
|
||||
const PLUGIN_SERVICE_ORCHESTRATOR_PREFIX: &str = "Codex verified that this exact connector was provided by the \
|
||||
@@ -44,6 +44,26 @@ pub struct TrustedTool {
|
||||
impl TrustedTool {
|
||||
/// Classification attached to this fragment's rendered text.
|
||||
pub const KIND: &str = "guardian.trusted_tool";
|
||||
|
||||
pub(crate) fn attestation(&self) -> String {
|
||||
let (prefix, source) = match &self.source {
|
||||
TrustedToolSource::UserConfiguration(path) => {
|
||||
(USER_CONFIGURATION_PREFIX, path.as_str())
|
||||
}
|
||||
TrustedToolSource::PluginServiceOrchestrator => (
|
||||
PLUGIN_SERVICE_ORCHESTRATOR_PREFIX,
|
||||
"plugin_service_orchestrator",
|
||||
),
|
||||
};
|
||||
format!(
|
||||
"{prefix}{TRUSTED_TOOL_SCOPE}\n{}",
|
||||
json!({
|
||||
"server": self.server,
|
||||
"connector_id": self.connector_id,
|
||||
"source": source,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for TrustedTool {
|
||||
@@ -64,24 +84,8 @@ impl ContextualUserFragment for TrustedTool {
|
||||
}
|
||||
|
||||
fn body(&self) -> ContentItem {
|
||||
let (prefix, source) = match &self.source {
|
||||
TrustedToolSource::UserConfiguration(path) => {
|
||||
(USER_CONFIGURATION_PREFIX, path.as_str())
|
||||
}
|
||||
TrustedToolSource::PluginServiceOrchestrator => (
|
||||
PLUGIN_SERVICE_ORCHESTRATOR_PREFIX,
|
||||
"plugin_service_orchestrator",
|
||||
),
|
||||
};
|
||||
input_text(truncate_entry(
|
||||
&format!(
|
||||
"{prefix}{TRUSTED_TOOL_SCOPE}\n{}",
|
||||
json!({
|
||||
"server": self.server,
|
||||
"connector_id": self.connector_id,
|
||||
"source": source,
|
||||
})
|
||||
),
|
||||
&self.attestation(),
|
||||
MAX_TRUSTED_TOOL_CONTEXT_TOKENS,
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user