This commit is contained in:
Roy Han
2026-03-16 14:33:47 -07:00
parent 546aa367d9
commit 80a4f69bce
12 changed files with 206 additions and 13 deletions
@@ -2032,6 +2032,16 @@
}
]
},
"sandbox_policy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicyMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -2410,6 +2420,14 @@
}
]
},
"SandboxPolicyMetadata": {
"enum": [
"read_only",
"sandbox",
"full_access"
],
"type": "string"
},
"ServiceTier": {
"enum": [
"fast",
@@ -10661,6 +10661,16 @@
}
]
},
"sandbox_policy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicyMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -11059,6 +11069,14 @@
}
]
},
"SandboxPolicyMetadata": {
"enum": [
"read_only",
"sandbox",
"full_access"
],
"type": "string"
},
"SandboxWorkspaceWrite": {
"properties": {
"exclude_slash_tmp": {
@@ -7406,6 +7406,16 @@
}
]
},
"sandbox_policy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicyMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -7804,6 +7814,14 @@
}
]
},
"SandboxPolicyMetadata": {
"enum": [
"read_only",
"sandbox",
"full_access"
],
"type": "string"
},
"SandboxWorkspaceWrite": {
"properties": {
"exclude_slash_tmp": {
@@ -873,6 +873,16 @@
}
]
},
"sandbox_policy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicyMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -999,6 +1009,14 @@
],
"type": "string"
},
"SandboxPolicyMetadata": {
"enum": [
"read_only",
"sandbox",
"full_access"
],
"type": "string"
},
"UserMessageType": {
"enum": [
"prompt",
@@ -939,6 +939,16 @@
}
]
},
"sandbox_policy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicyMetadata"
},
{
"type": "null"
}
]
},
"user_message_type": {
"anyOf": [
{
@@ -1073,6 +1083,14 @@
],
"type": "string"
},
"SandboxPolicyMetadata": {
"enum": [
"read_only",
"sandbox",
"full_access"
],
"type": "string"
},
"ServiceTier": {
"enum": [
"fast",
@@ -2,6 +2,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReviewDecisionMetadata } from "./ReviewDecisionMetadata";
import type { SandboxPolicyMetadata } from "./SandboxPolicyMetadata";
import type { UserMessageType } from "./UserMessageType";
export type ResponseItemMetadata = { user_message_type?: UserMessageType, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, };
export type ResponseItemMetadata = { user_message_type?: UserMessageType, sandbox_policy?: SandboxPolicyMetadata, is_tool_call_escalated?: boolean, review_decision?: ReviewDecisionMetadata, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SandboxPolicyMetadata = "read_only" | "sandbox" | "full_access";
@@ -61,6 +61,7 @@ export type { ResponseItem } from "./ResponseItem";
export type { ResponseItemMetadata } from "./ResponseItemMetadata";
export type { ReviewDecision } from "./ReviewDecision";
export type { ReviewDecisionMetadata } from "./ReviewDecisionMetadata";
export type { SandboxPolicyMetadata } from "./SandboxPolicyMetadata";
export type { ServerNotification } from "./ServerNotification";
export type { ServerRequest } from "./ServerRequest";
export type { ServiceTier } from "./ServiceTier";
+62 -12
View File
@@ -323,6 +323,7 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::models::ReviewDecisionMetadata;
use codex_protocol::models::SandboxPolicyMetadata;
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::protocol::CodexErrorInfo;
use codex_protocol::protocol::InitialHistory;
@@ -1012,6 +1013,31 @@ fn stamp_user_message_type_on_input_item(item: &mut ResponseInputItem, kind: Use
*metadata = Some(metadata_value);
}
fn sandbox_policy_to_metadata(policy: &SandboxPolicy) -> SandboxPolicyMetadata {
match policy {
SandboxPolicy::ReadOnly { .. } => SandboxPolicyMetadata::ReadOnly,
SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::ExternalSandbox { .. } => {
SandboxPolicyMetadata::Sandbox
}
SandboxPolicy::DangerFullAccess => SandboxPolicyMetadata::FullAccess,
}
}
fn stamp_sandbox_policy_on_input_item(
item: &mut ResponseInputItem,
sandbox_policy: SandboxPolicyMetadata,
) {
let ResponseInputItem::Message { role, metadata, .. } = item else {
return;
};
if role != "user" {
return;
}
let mut metadata_value = metadata.take().unwrap_or_default();
metadata_value.sandbox_policy = Some(sandbox_policy);
*metadata = Some(metadata_value);
}
fn review_decision_to_metadata(decision: &ReviewDecision) -> ReviewDecisionMetadata {
match decision {
ReviewDecision::Approved => ReviewDecisionMetadata::Approved,
@@ -3383,7 +3409,7 @@ impl Session {
async fn stamp_tool_approval_metadata(
&self,
_turn_context: &TurnContext,
turn_context: &TurnContext,
response_item: ResponseItem,
) -> ResponseItem {
if !self.enabled(Feature::ItemMetadata) {
@@ -3410,6 +3436,9 @@ impl Session {
| ResponseItem::CustomToolCall { metadata, .. } => metadata.clone().unwrap_or_default(),
_ => return response_item,
};
metadata.sandbox_policy = Some(sandbox_policy_to_metadata(
turn_context.sandbox_policy.get(),
));
match outcome {
Some(review_decision) => {
@@ -3421,7 +3450,7 @@ impl Session {
metadata.review_decision = None;
}
None => {
return response_item;
return stamp_tool_metadata_on_response_item(response_item, metadata);
}
}
@@ -3944,13 +3973,18 @@ impl Session {
response_item: ResponseItem,
user_message_type: Option<UserMessageType>,
) {
let user_message_type = if self.enabled(Feature::ItemMetadata) {
user_message_type
let (user_message_type, sandbox_policy) = if self.enabled(Feature::ItemMetadata) {
(
user_message_type,
Some(sandbox_policy_to_metadata(
turn_context.sandbox_policy.get(),
)),
)
} else {
None
(None, None)
};
let response_item = match (response_item, user_message_type.clone()) {
let response_item = match (response_item, user_message_type.clone(), sandbox_policy) {
(
ResponseItem::Message {
id,
@@ -3960,10 +3994,12 @@ impl Session {
end_turn,
phase,
},
Some(kind),
user_message_type,
sandbox_policy,
) if role == "user" => {
let mut metadata = metadata.unwrap_or_default();
metadata.user_message_type = Some(kind);
metadata.user_message_type = user_message_type;
metadata.sandbox_policy = sandbox_policy;
ResponseItem::Message {
id,
role,
@@ -3973,7 +4009,7 @@ impl Session {
phase,
}
}
(response_item, _) => response_item,
(response_item, _, _) => response_item,
};
// Persist the user message to history, but emit the turn item from `UserInput` so
@@ -4061,7 +4097,7 @@ impl Session {
return Err(SteerInputError::NoActiveTurn(input));
};
let Some((active_turn_id, _)) = active_turn.tasks.first() else {
let Some((active_turn_id, active_task)) = active_turn.tasks.first() else {
return Err(SteerInputError::NoActiveTurn(input));
};
@@ -4070,18 +4106,22 @@ impl Session {
{
return Err(SteerInputError::ExpectedTurnMismatch {
expected: expected_turn_id.to_string(),
actual: active_turn_id.clone(),
actual: active_turn_id.to_string(),
});
}
let mut input_item: ResponseInputItem = input.into();
if self.enabled(Feature::ItemMetadata) {
stamp_user_message_type_on_input_item(&mut input_item, UserMessageType::PromptSteering);
stamp_sandbox_policy_on_input_item(
&mut input_item,
sandbox_policy_to_metadata(active_task.turn_context.sandbox_policy.get()),
);
}
let mut turn_state = active_turn.turn_state.lock().await;
turn_state.push_pending_input(input_item, Some(UserMessageType::PromptSteering));
Ok(active_turn_id.clone())
Ok(active_turn_id.to_string())
}
/// Returns the input if there was no task running to inject into
@@ -4092,6 +4132,9 @@ impl Session {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let sandbox_policy = at.tasks.first().map(|(_, turn_context)| {
sandbox_policy_to_metadata(turn_context.turn_context.sandbox_policy.get())
});
let mut ts = at.turn_state.lock().await;
for mut item in input {
let user_message_type = match &item {
@@ -4102,6 +4145,9 @@ impl Session {
&& let Some(kind) = user_message_type.clone()
{
stamp_user_message_type_on_input_item(&mut item, kind);
if let Some(sandbox_policy) = sandbox_policy.clone() {
stamp_sandbox_policy_on_input_item(&mut item, sandbox_policy);
}
}
ts.push_pending_input(item, user_message_type);
}
@@ -5887,6 +5933,10 @@ pub(crate) async fn run_turn(
let mut initial_input_for_turn: ResponseInputItem = ResponseInputItem::from(input.clone());
if sess.enabled(Feature::ItemMetadata) {
stamp_user_message_type_on_input_item(&mut initial_input_for_turn, UserMessageType::Prompt);
stamp_sandbox_policy_on_input_item(
&mut initial_input_for_turn,
sandbox_policy_to_metadata(turn_context.sandbox_policy.get()),
);
}
let response_item: ResponseItem = initial_input_for_turn.clone().into();
sess.record_user_prompt_and_emit_turn_item(
+20
View File
@@ -3940,9 +3940,26 @@ fn review_decision_metadata_mapping_is_stable() {
);
}
#[test]
fn sandbox_policy_metadata_mapping_is_stable() {
assert_eq!(
sandbox_policy_to_metadata(&SandboxPolicy::DangerFullAccess),
codex_protocol::models::SandboxPolicyMetadata::FullAccess
);
assert_eq!(
sandbox_policy_to_metadata(&SandboxPolicy::new_read_only_policy()),
codex_protocol::models::SandboxPolicyMetadata::ReadOnly
);
assert_eq!(
sandbox_policy_to_metadata(&SandboxPolicy::new_workspace_write_policy()),
codex_protocol::models::SandboxPolicyMetadata::Sandbox
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enabled() {
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get());
Arc::get_mut(&mut sess)
.expect("session should be uniquely owned in this test")
.features
@@ -3993,6 +4010,7 @@ async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enable
} if metadata.is_tool_call_escalated == Some(true)
&& metadata.review_decision
== Some(codex_protocol::models::ReviewDecisionMetadata::Denied)
&& metadata.sandbox_policy == Some(expected_sandbox_policy)
)
));
}
@@ -4000,6 +4018,7 @@ async fn tool_call_metadata_stamps_escalated_review_decision_when_feature_enable
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() {
let (mut sess, tc, rx) = make_session_and_context_with_rx().await;
let expected_sandbox_policy = sandbox_policy_to_metadata(tc.sandbox_policy.get());
Arc::get_mut(&mut sess)
.expect("session should be uniquely owned in this test")
.features
@@ -4047,6 +4066,7 @@ async fn tool_call_metadata_stamps_non_escalated_false_when_feature_enabled() {
..
} if metadata.is_tool_call_escalated == Some(false)
&& metadata.review_decision.is_none()
&& metadata.sandbox_policy == Some(expected_sandbox_policy)
)
));
}
+14
View File
@@ -344,6 +344,13 @@ async fn user_message_type_prompt_steering_metadata_is_emitted_when_feature_enab
.and_then(Value::as_str),
Some("prompt_steering")
);
assert_eq!(
steered_message
.get("metadata")
.and_then(|metadata| metadata.get("sandbox_policy"))
.and_then(Value::as_str),
Some("full_access")
);
Ok(())
}
@@ -476,6 +483,13 @@ async fn user_message_type_prompt_queued_metadata_is_emitted_when_feature_enable
.and_then(Value::as_str),
Some("prompt_queued")
);
assert_eq!(
queued_message
.get("metadata")
.and_then(|metadata| metadata.get("sandbox_policy"))
.and_then(Value::as_str),
Some("full_access")
);
Ok(())
}
+12
View File
@@ -275,6 +275,15 @@ pub enum ReviewDecisionMetadata {
DeniedWithNetworkPolicyDeny,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum SandboxPolicyMetadata {
ReadOnly,
Sandbox,
FullAccess,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -282,6 +291,9 @@ pub struct ResponseItemMetadata {
pub user_message_type: Option<UserMessageType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sandbox_policy: Option<SandboxPolicyMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub is_tool_call_escalated: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]