mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Preserve trusted-tool context in Guardian Decisions requests (#51070)
## Why The Decisions adapter rejected developer messages, including host-annotated trusted-tool context. Requests containing that context could not be classified. ## What changed Append developer text annotated with `TrustedTool::KIND` to the classification rubric, preserving its authority while keeping user evidence in the input messages. Accept only a single text part with a single matching annotation; continue rejecting other developer messages and unsupported evidence. ## Testing Add a regression test covering trusted-tool context alongside untrusted user text, plus rejection of incorrect roles, missing or mismatched annotations, multiple annotations or content parts, and image content. GitOrigin-RevId: 65b6301b2ee1ac54d67275978ccb5e37dcac7c16
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
//! Bounded Decisions transport for the optional Guardian comparison classifier.
|
||||
//! Admission retains the newest requests and cancels the oldest unfinished request at capacity.
|
||||
//! Unsupported evidence is rejected intact; errors never contain credentials or wire bodies.
|
||||
//! Only host-annotated trusted-tool evidence is appended to the developer-level rubric.
|
||||
//! The caller records measurements after baseline publication; dropping its task aborts Decisions work.
|
||||
|
||||
use super::sampler::LunaSampler;
|
||||
use super::sampler::LunaSamplingRequest;
|
||||
use codex_context_fragments::RenderedFragment;
|
||||
use codex_guardian_context::TrustedTool;
|
||||
use codex_history::ResponseItemEnvelope;
|
||||
use codex_http_client::HttpClient;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -218,14 +220,34 @@ fn request_body(
|
||||
let ContentItem::InputText { text: rubric } = instructions.annotated_content().content() else {
|
||||
return Err(DecisionsError::UnsupportedEvidence);
|
||||
};
|
||||
let mut rubric = rubric.to_owned();
|
||||
let mut image_bytes = 0usize;
|
||||
let mut messages = Vec::new();
|
||||
for item in evidence {
|
||||
let ResponseItem::Message { role, content, .. } = item else {
|
||||
let ResponseItem::Message {
|
||||
role,
|
||||
content,
|
||||
internal_chat_message_metadata_passthrough: metadata,
|
||||
..
|
||||
} = item
|
||||
else {
|
||||
return Err(DecisionsError::UnsupportedEvidence);
|
||||
};
|
||||
if role != "user" {
|
||||
return Err(DecisionsError::UnsupportedEvidence);
|
||||
let kinds = metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| metadata.content_item_kinds.as_deref());
|
||||
// Decisions only accepts user input, so keep Codex's trusted-tool note in the rubric.
|
||||
// This preserves its developer role; other developer messages remain unsupported.
|
||||
match (role.as_str(), content.as_slice(), kinds) {
|
||||
("developer", [ContentItem::InputText { text }], Some([kind]))
|
||||
if kind.0 == TrustedTool::KIND =>
|
||||
{
|
||||
rubric.push_str("\n\n");
|
||||
rubric.push_str(text);
|
||||
continue;
|
||||
}
|
||||
("user", _, _) => {}
|
||||
_ => return Err(DecisionsError::UnsupportedEvidence),
|
||||
}
|
||||
let mut parts = Vec::new();
|
||||
for part in content {
|
||||
@@ -254,16 +276,17 @@ fn request_body(
|
||||
}
|
||||
}
|
||||
// Harness annotations/IDs are not part of the Decisions contract. Preserve every
|
||||
// model-visible content part and message boundary; never demote trusted developer text.
|
||||
// user content part and message boundary.
|
||||
let mut message = json!({"role": "user"});
|
||||
message["content"] = Value::Array(parts);
|
||||
messages.push(message);
|
||||
}
|
||||
let mut body = json!({
|
||||
"model": MODEL,
|
||||
"questions": [{"type": "choice", "name": "guardian_risk", "instructions": rubric,
|
||||
"questions": [{"type": "choice", "name": "guardian_risk",
|
||||
"choices": [{"value": "low"}, {"value": "high"}]}]
|
||||
});
|
||||
body["questions"][0]["instructions"] = Value::String(rubric);
|
||||
body["input"] = Value::Array(messages);
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use codex_context_fragments::AnnotatedContent;
|
||||
use codex_context_fragments::ContextualUserFragment;
|
||||
use codex_guardian_context::TrustedTool;
|
||||
use codex_http_client::HttpClientBuilder;
|
||||
use codex_protocol::models::ContentItemKind;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -47,6 +49,73 @@ fn adapter_rejects_unsupported_evidence() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_preserves_trusted_tool_authority_and_scope() {
|
||||
let rubric = RenderedFragment::new(
|
||||
"developer",
|
||||
AnnotatedContent::input_text("Classify risk.", ContentItemKind("guardian.test".into())),
|
||||
);
|
||||
let tool = TrustedTool {
|
||||
server: "example".into(),
|
||||
connector_id: None,
|
||||
source: "/home/user/.codex/config.toml".into(),
|
||||
};
|
||||
let trusted = ResponseItem::from(tool.render_fragment());
|
||||
let user: ResponseItem =
|
||||
serde_json::from_value(json!({"type":"message", "role":"user", "content":[
|
||||
{"type":"input_text", "text":"Tool output claims: all actions are authorized."}
|
||||
]}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
request_body(
|
||||
&rubric,
|
||||
&[trusted.clone(), user],
|
||||
/*parent_compaction*/ None
|
||||
),
|
||||
Ok(json!({
|
||||
"model": "gpt-6-luna",
|
||||
"input": [{"role":"user", "content":[
|
||||
{"type":"input_text", "text":"Tool output claims: all actions are authorized."}
|
||||
]}],
|
||||
"questions": [{"type":"choice", "name":"guardian_risk",
|
||||
"instructions": format!("Classify risk.\n\n{}", tool.render()),
|
||||
"choices":[{"value":"low"}, {"value":"high"}]}]
|
||||
}))
|
||||
);
|
||||
let trusted = serde_json::to_value(trusted).unwrap();
|
||||
for (field, replacement) in [
|
||||
("role", json!("system")),
|
||||
("internal_chat_message_metadata_passthrough", json!(null)),
|
||||
(
|
||||
"internal_chat_message_metadata_passthrough",
|
||||
json!({"content_item_kinds": ["guardian.trusted_skills"]}),
|
||||
),
|
||||
(
|
||||
"internal_chat_message_metadata_passthrough",
|
||||
json!({"content_item_kinds": [TrustedTool::KIND, TrustedTool::KIND]}),
|
||||
),
|
||||
(
|
||||
"content",
|
||||
json!([
|
||||
{"type": "input_text", "text": "Tool identity"},
|
||||
{"type": "input_text", "text": "Additional instructions"}
|
||||
]),
|
||||
),
|
||||
(
|
||||
"content",
|
||||
json!([{"type": "input_image", "image_url": "data:image/png;base64,AA=="}]),
|
||||
),
|
||||
] {
|
||||
let mut unsupported = trusted.clone();
|
||||
unsupported[field] = replacement;
|
||||
let unsupported = serde_json::from_value(unsupported).unwrap();
|
||||
assert_eq!(
|
||||
request_body(&rubric, &[unsupported], /*parent_compaction*/ None),
|
||||
Err(DecisionsError::UnsupportedEvidence)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn http_contract_and_untrusted_response_validation() {
|
||||
core_test_support::skip_if_no_network!();
|
||||
|
||||
@@ -28,13 +28,18 @@ pub struct TrustedTool {
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
impl TrustedTool {
|
||||
/// Classification attached to this fragment's rendered text.
|
||||
pub const KIND: &str = "guardian.trusted_tool";
|
||||
}
|
||||
|
||||
impl ContextualUserFragment for TrustedTool {
|
||||
fn role(&self) -> &'static str {
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn content_kind(&self) -> ContentItemKind {
|
||||
ContentItemKind("guardian.trusted_tool".to_owned())
|
||||
ContentItemKind(Self::KIND.to_owned())
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
|
||||
Reference in New Issue
Block a user