mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Enable untrusted context tool outputs by default (#60487)
## What changed Mark `untrusted_as_tool_output` as stable and enable it by default. Untrusted additional context, such as browser information and goal objectives, now reaches the model as `function_call_output` items in the `agent` namespace instead of user messages. Setting `features.untrusted_as_tool_output = false` preserves the message format. ## Testing Update core, app-server, TUI, and Python SDK test expectations for the default output format. Cover context persistence, deduplication, goal continuation, and explicit opt-out behavior. GitOrigin-RevId: 30828351e2ed584928f6055b6dcf276d8288c0fb
This commit is contained in:
@@ -228,24 +228,37 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history(
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
RolloutItem::ResponseItem(envelope) => {
|
||||
let ResponseItem::Message { role, content, .. } = &envelope.item else {
|
||||
return None;
|
||||
let context_kind = match &envelope.item {
|
||||
ResponseItem::Message { role, content, .. } => {
|
||||
content.iter().find_map(|item| {
|
||||
let ContentItem::InputText { text } = item else {
|
||||
return None;
|
||||
};
|
||||
text.contains(application_context_text)
|
||||
.then_some(role.as_str())
|
||||
})
|
||||
}
|
||||
ResponseItem::FunctionCallOutput {
|
||||
name,
|
||||
namespace,
|
||||
output,
|
||||
..
|
||||
} if name.as_deref() == Some("untrusted_context")
|
||||
&& namespace.as_deref() == Some("agent")
|
||||
&& output.text_content() == Some(untrusted_context_text) =>
|
||||
{
|
||||
Some("function_call_output")
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
content.iter().find_map(|item| {
|
||||
let ContentItem::InputText { text } = item else {
|
||||
return None;
|
||||
};
|
||||
(text.contains(application_context_text)
|
||||
|| text.contains(untrusted_context_text))
|
||||
.then(|| {
|
||||
(
|
||||
role.as_str(),
|
||||
envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.client_authored),
|
||||
)
|
||||
})
|
||||
context_kind.map(|kind| {
|
||||
(
|
||||
kind,
|
||||
envelope
|
||||
.metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.client_authored),
|
||||
)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@@ -253,7 +266,10 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history(
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
persisted_additional_context,
|
||||
vec![("developer", Some(true)), ("user", Some(false))]
|
||||
vec![
|
||||
("developer", Some(true)),
|
||||
("function_call_output", Some(false))
|
||||
]
|
||||
);
|
||||
|
||||
let injected_value = serde_json::to_value(&injected_item)?;
|
||||
|
||||
@@ -815,10 +815,9 @@ async fn turn_start_additional_context_flows_to_model_input(
|
||||
let ThreadStartResponse { thread, .. } = mcp
|
||||
.start_thread(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
config: Some(HashMap::from([(
|
||||
"features.untrusted_as_tool_output".to_owned(),
|
||||
json!(tool_outputs),
|
||||
)])),
|
||||
config: (!tool_outputs).then(|| {
|
||||
HashMap::from([("features.untrusted_as_tool_output".to_owned(), json!(false))])
|
||||
}),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -557,11 +557,7 @@ async fn turn_steer_rejects_context_only_input_without_merging_context() -> Resu
|
||||
let body = response_requests[1]
|
||||
.body_json::<Value>()
|
||||
.context("request body should be JSON")?;
|
||||
assert!(
|
||||
!body
|
||||
.to_string()
|
||||
.contains("<external_browser_info>tab one</external_browser_info>")
|
||||
);
|
||||
assert!(!body.to_string().contains("tab one"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::received_responses_requests;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::responses::strip_metadata_from_json;
|
||||
use core_test_support::responses::strip_response_item_ids_from_json;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
@@ -113,7 +115,9 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
|
||||
|
||||
let request = request.single_request();
|
||||
assert!(request.has_content_kinds(&["additional_content.automation_info"]));
|
||||
assert!(request.has_content_kinds(&["additional_content.browser_info"]));
|
||||
let outputs = request.inputs_of_type("function_call_output");
|
||||
assert_eq!(outputs.len(), 1);
|
||||
assert_browser_context_actor_output(&outputs[0]);
|
||||
assert!(request.has_content_kinds(&["user.text"]));
|
||||
insta::assert_snapshot!(
|
||||
"additional_context_simple_input",
|
||||
@@ -134,10 +138,7 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re
|
||||
);
|
||||
assert_eq!(
|
||||
request.message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>tab one</external_browser_info>",
|
||||
"inspect the active tab",
|
||||
]
|
||||
vec!["inspect the active tab"]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -639,20 +640,47 @@ async fn additional_context_is_deduplicated_between_turns_while_retained(
|
||||
})
|
||||
.await;
|
||||
|
||||
let first_request = first_request.single_request();
|
||||
let second_request = second_request.single_request();
|
||||
assert_eq!(
|
||||
first_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>same tab</external_browser_info>",
|
||||
"first turn",
|
||||
]
|
||||
first_request.message_input_texts("user"),
|
||||
vec!["first turn"]
|
||||
);
|
||||
assert_eq!(
|
||||
second_request.single_request().message_input_texts("user"),
|
||||
vec![
|
||||
"<external_browser_info>same tab</external_browser_info>",
|
||||
"first turn",
|
||||
"second turn",
|
||||
]
|
||||
second_request.message_input_texts("user"),
|
||||
vec!["first turn", "second turn"]
|
||||
);
|
||||
let mut expected_outputs = vec![json!({
|
||||
"type": "function_call_output",
|
||||
"namespace": "agent",
|
||||
"name": first_name.unwrap_or("browser_info"),
|
||||
"output": "same tab",
|
||||
})];
|
||||
assert_eq!(
|
||||
first_request
|
||||
.inputs_of_type("function_call_output")
|
||||
.into_iter()
|
||||
.map(strip_response_item_ids_from_json)
|
||||
.map(strip_metadata_from_json)
|
||||
.collect::<Vec<_>>(),
|
||||
expected_outputs,
|
||||
);
|
||||
if first_name != second_name {
|
||||
expected_outputs.push(json!({
|
||||
"type": "function_call_output",
|
||||
"namespace": "agent",
|
||||
"name": second_name.unwrap_or("browser_info"),
|
||||
"output": "same tab",
|
||||
}));
|
||||
}
|
||||
assert_eq!(
|
||||
second_request
|
||||
.inputs_of_type("function_call_output")
|
||||
.into_iter()
|
||||
.map(strip_response_item_ids_from_json)
|
||||
.map(strip_metadata_from_json)
|
||||
.collect::<Vec<_>>(),
|
||||
expected_outputs,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -214,7 +214,7 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()>
|
||||
message developer ["multi_agent.mode_instructions"]
|
||||
message user ["environments.environment_context"]
|
||||
message developer ["additional_content.automation_info"]
|
||||
message user ["additional_content.browser_info"]
|
||||
function_call_output - null
|
||||
message user ["user.text","user.image","user.audio"]
|
||||
message developer ["rollout_budget.remaining_tokens"]
|
||||
message developer ["current_time.reminder"]
|
||||
|
||||
@@ -572,10 +572,12 @@ async fn additional_context_tool_outputs(tool_outputs: bool) -> Result<()> {
|
||||
.with_config(move |config| {
|
||||
configure_scenario_catalog(config);
|
||||
config.include_environment_context = false;
|
||||
config
|
||||
.features
|
||||
.set_enabled(Feature::UntrustedAsToolOutput, tool_outputs)
|
||||
.expect("test config should allow feature update");
|
||||
if !tool_outputs {
|
||||
config
|
||||
.features
|
||||
.disable(Feature::UntrustedAsToolOutput)
|
||||
.expect("test config should allow feature update");
|
||||
}
|
||||
})
|
||||
.build_with_auto_env(&server)
|
||||
.await?;
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@ Scenario: additional context is inserted before the user turn input.
|
||||
</tools>
|
||||
02:message/developer:
|
||||
<automation_info>run one</automation_info>
|
||||
03:message/user:
|
||||
<external_browser_info>tab one</external_browser_info>
|
||||
03:function_call_output/agent.browser_info:tab one
|
||||
04:message/user:
|
||||
inspect the active tab
|
||||
|
||||
@@ -1283,8 +1283,8 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::UntrustedAsToolOutput,
|
||||
key: "untrusted_as_tool_output",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ExecutedToolCallMetadata,
|
||||
|
||||
@@ -1441,9 +1441,16 @@ goals = true
|
||||
retry_request_index + 1
|
||||
};
|
||||
assert!(
|
||||
user_input_texts(&request_bodies[goal_continuation_request_index])
|
||||
request_bodies[goal_continuation_request_index]["input"]
|
||||
.as_array()
|
||||
.expect("goal continuation model input")
|
||||
.iter()
|
||||
.any(|text| text.contains(RETRY_GOAL)),
|
||||
.any(|item| {
|
||||
item["type"] == "function_call_output"
|
||||
&& item["namespace"] == "agent"
|
||||
&& item["name"] == "goal_objective"
|
||||
&& item["output"] == RETRY_GOAL
|
||||
}),
|
||||
"inherited goal continuation should resume after the explicit retry"
|
||||
);
|
||||
|
||||
|
||||
@@ -74,9 +74,12 @@ def test_private_goal_operation_coalesces_runtime_continuations(tmp_path) -> Non
|
||||
"result": (result.id, result.status, result.final_response),
|
||||
"messages": agent_message_texts(events),
|
||||
"request_count": len(requests),
|
||||
"objective_reached_model": (
|
||||
"<external_goal_objective>Improve benchmark coverage</external_goal_objective>"
|
||||
in requests[0].message_input_texts("user")
|
||||
"objective_reached_model": any(
|
||||
item.get("type") == "function_call_output"
|
||||
and item.get("namespace") == "agent"
|
||||
and item.get("name") == "goal_objective"
|
||||
and item.get("output") == "Improve benchmark coverage"
|
||||
for item in requests[0].input()
|
||||
),
|
||||
"steering_reached_model": any(
|
||||
"Continue working toward the active thread goal." in text
|
||||
|
||||
Reference in New Issue
Block a user