guardian: move approval request formatting out of core

This commit is contained in:
Dylan Hurd
2026-06-11 23:49:10 -07:00
parent 1fd20fdb91
commit 70c858fc28
9 changed files with 554 additions and 522 deletions

7
codex-rs/Cargo.lock generated
View File

@@ -2596,6 +2596,7 @@ dependencies = [
"codex-features",
"codex-feedback",
"codex-git-utils",
"codex-guardian",
"codex-home",
"codex-hooks",
"codex-image-generation-extension",
@@ -3071,8 +3072,14 @@ dependencies = [
name = "codex-guardian"
version = "0.0.0"
dependencies = [
"codex-analytics",
"codex-extension-api",
"codex-protocol",
"codex-shell-command",
"codex-utils-absolute-path",
"codex-utils-output-truncation",
"serde",
"serde_json",
]
[[package]]

View File

@@ -46,6 +46,7 @@ codex-models-manager = { workspace = true }
codex-shell-command = { workspace = true }
codex-execpolicy = { workspace = true }
codex-git-utils = { workspace = true }
codex-guardian = { workspace = true }
codex-hooks = { workspace = true }
codex-install-context = { workspace = true }
codex-network-proxy = { workspace = true }

View File

@@ -1,455 +1,7 @@
use std::path::Path;
use codex_analytics::GuardianReviewedAction;
use codex_extension_api::ApprovalReviewMcpAnnotations as GuardianMcpAnnotations;
use codex_extension_api::ApprovalReviewNetworkAccessTrigger as GuardianNetworkAccessTrigger;
use codex_extension_api::ApprovalReviewRequest as GuardianApprovalRequest;
use codex_protocol::approvals::GuardianAssessmentAction;
use codex_protocol::approvals::GuardianCommandSource;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Serialize;
use serde_json::Value;
use super::GUARDIAN_MAX_ACTION_STRING_TOKENS;
use super::prompt::guardian_truncate_text;
#[derive(Serialize)]
struct CommandApprovalAction<'a> {
tool: &'a str,
command: &'a [String],
cwd: &'a Path,
sandbox_permissions: crate::sandboxing::SandboxPermissions,
#[serde(skip_serializing_if = "Option::is_none")]
additional_permissions: Option<&'a AdditionalPermissionProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
justification: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tty: Option<bool>,
}
#[cfg(unix)]
#[derive(Serialize)]
struct ExecveApprovalAction<'a> {
tool: &'a str,
program: &'a str,
argv: &'a [String],
cwd: &'a Path,
#[serde(skip_serializing_if = "Option::is_none")]
additional_permissions: Option<&'a AdditionalPermissionProfile>,
}
#[derive(Serialize)]
struct McpToolCallApprovalAction<'a> {
tool: &'static str,
server: &'a str,
tool_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
arguments: Option<&'a Value>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_id: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_name: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_description: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_title: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_description: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<&'a GuardianMcpAnnotations>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct NetworkAccessApprovalAction<'a> {
tool: &'static str,
target: &'a str,
host: &'a str,
protocol: NetworkApprovalProtocol,
port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
trigger: Option<&'a GuardianNetworkAccessTrigger>,
}
#[derive(Serialize)]
struct RequestPermissionsApprovalAction<'a> {
tool: &'static str,
turn_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<&'a String>,
permissions: &'a RequestPermissionProfile,
}
fn serialize_guardian_action(value: impl Serialize) -> serde_json::Result<Value> {
serde_json::to_value(value)
}
fn serialize_command_guardian_action(
tool: &'static str,
command: &[String],
cwd: &Path,
sandbox_permissions: crate::sandboxing::SandboxPermissions,
additional_permissions: Option<&AdditionalPermissionProfile>,
justification: Option<&String>,
tty: Option<bool>,
) -> serde_json::Result<Value> {
serialize_guardian_action(CommandApprovalAction {
tool,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
tty,
})
}
fn command_assessment_action(
source: GuardianCommandSource,
command: &[String],
cwd: &AbsolutePathBuf,
) -> GuardianAssessmentAction {
GuardianAssessmentAction::Command {
source,
command: codex_shell_command::parse_command::shlex_join(command),
cwd: cwd.clone(),
}
}
#[cfg(unix)]
fn guardian_command_source_tool_name(source: GuardianCommandSource) -> &'static str {
match source {
GuardianCommandSource::Shell => "shell",
GuardianCommandSource::UnifiedExec => "exec_command",
}
}
fn truncate_guardian_action_value(value: Value) -> (Value, bool) {
match value {
Value::String(text) => {
let (text, truncated) =
guardian_truncate_text(&text, GUARDIAN_MAX_ACTION_STRING_TOKENS);
(Value::String(text), truncated)
}
Value::Array(values) => {
let mut truncated = false;
let values = values
.into_iter()
.map(|value| {
let (value, value_truncated) = truncate_guardian_action_value(value);
truncated |= value_truncated;
value
})
.collect::<Vec<_>>();
(Value::Array(values), truncated)
}
Value::Object(values) => {
let mut entries = values.into_iter().collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut truncated = false;
let values = entries
.into_iter()
.map(|(key, value)| {
let (value, value_truncated) = truncate_guardian_action_value(value);
truncated |= value_truncated;
(key, value)
})
.collect();
(Value::Object(values), truncated)
}
other => (other, false),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FormattedGuardianAction {
pub(crate) text: String,
pub(crate) truncated: bool,
}
pub(crate) fn guardian_approval_request_to_json(
action: &GuardianApprovalRequest,
) -> serde_json::Result<Value> {
match action {
GuardianApprovalRequest::Shell {
id: _,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
} => serialize_command_guardian_action(
"shell",
command,
cwd,
*sandbox_permissions,
additional_permissions.as_ref(),
justification.as_ref(),
/*tty*/ None,
),
GuardianApprovalRequest::ExecCommand {
id: _,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
tty,
} => serialize_command_guardian_action(
"exec_command",
command,
cwd,
*sandbox_permissions,
additional_permissions.as_ref(),
justification.as_ref(),
Some(*tty),
),
#[cfg(unix)]
GuardianApprovalRequest::Execve {
id: _,
source,
program,
argv,
cwd,
additional_permissions,
} => serialize_guardian_action(ExecveApprovalAction {
tool: guardian_command_source_tool_name(*source),
program,
argv,
cwd,
additional_permissions: additional_permissions.as_ref(),
}),
GuardianApprovalRequest::ApplyPatch {
id: _,
cwd,
files,
patch,
} => Ok(serde_json::json!({
"tool": "apply_patch",
"cwd": cwd,
"files": files,
"patch": patch,
})),
GuardianApprovalRequest::NetworkAccess {
id: _,
turn_id: _,
target,
host,
protocol,
port,
trigger,
} => serialize_guardian_action(NetworkAccessApprovalAction {
tool: "network_access",
target,
host,
protocol: *protocol,
port: *port,
trigger: trigger.as_ref(),
}),
GuardianApprovalRequest::McpToolCall {
id: _,
server,
tool_name,
arguments,
connector_id,
connector_name,
connector_description,
tool_title,
tool_description,
annotations,
} => serialize_guardian_action(McpToolCallApprovalAction {
tool: "mcp_tool_call",
server,
tool_name,
arguments: arguments.as_ref(),
connector_id: connector_id.as_ref(),
connector_name: connector_name.as_ref(),
connector_description: connector_description.as_ref(),
tool_title: tool_title.as_ref(),
tool_description: tool_description.as_ref(),
annotations: annotations.as_ref(),
}),
GuardianApprovalRequest::RequestPermissions {
id: _,
turn_id,
reason,
permissions,
} => serialize_guardian_action(RequestPermissionsApprovalAction {
tool: "request_permissions",
turn_id,
reason: reason.as_ref(),
permissions,
}),
}
}
pub(crate) fn guardian_assessment_action(
action: &GuardianApprovalRequest,
) -> GuardianAssessmentAction {
match action {
GuardianApprovalRequest::Shell { command, cwd, .. } => {
command_assessment_action(GuardianCommandSource::Shell, command, cwd)
}
GuardianApprovalRequest::ExecCommand { command, cwd, .. } => {
command_assessment_action(GuardianCommandSource::UnifiedExec, command, cwd)
}
#[cfg(unix)]
GuardianApprovalRequest::Execve {
source,
program,
argv,
cwd,
..
} => GuardianAssessmentAction::Execve {
source: *source,
program: program.clone(),
argv: argv.clone(),
cwd: cwd.clone(),
},
GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => {
GuardianAssessmentAction::ApplyPatch {
cwd: cwd.clone(),
files: files.clone(),
}
}
GuardianApprovalRequest::NetworkAccess {
id: _id,
turn_id: _turn_id,
target,
host,
protocol,
port,
trigger: _trigger,
} => GuardianAssessmentAction::NetworkAccess {
target: target.clone(),
host: host.clone(),
protocol: *protocol,
port: *port,
},
GuardianApprovalRequest::McpToolCall {
server,
tool_name,
connector_id,
connector_name,
tool_title,
..
} => GuardianAssessmentAction::McpToolCall {
server: server.clone(),
tool_name: tool_name.clone(),
connector_id: connector_id.clone(),
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions {
reason,
permissions,
..
} => GuardianAssessmentAction::RequestPermissions {
reason: reason.clone(),
permissions: permissions.clone(),
},
}
}
pub(crate) fn guardian_reviewed_action(
request: &GuardianApprovalRequest,
) -> GuardianReviewedAction {
match request {
GuardianApprovalRequest::Shell {
sandbox_permissions,
additional_permissions,
..
} => GuardianReviewedAction::Shell {
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
},
GuardianApprovalRequest::ExecCommand {
sandbox_permissions,
additional_permissions,
tty,
..
} => GuardianReviewedAction::UnifiedExec {
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
tty: *tty,
},
#[cfg(unix)]
GuardianApprovalRequest::Execve {
source,
program,
additional_permissions,
..
} => GuardianReviewedAction::Execve {
source: *source,
program: program.clone(),
additional_permissions: additional_permissions.clone(),
},
GuardianApprovalRequest::ApplyPatch { .. } => GuardianReviewedAction::ApplyPatch {},
GuardianApprovalRequest::NetworkAccess { protocol, port, .. } => {
GuardianReviewedAction::NetworkAccess {
protocol: *protocol,
port: *port,
}
}
GuardianApprovalRequest::McpToolCall {
server,
tool_name,
connector_id,
connector_name,
tool_title,
..
} => GuardianReviewedAction::McpToolCall {
server: server.clone(),
tool_name: tool_name.clone(),
connector_id: connector_id.clone(),
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions { .. } => {
GuardianReviewedAction::RequestPermissions {}
}
}
}
pub(crate) fn guardian_request_target_item_id(request: &GuardianApprovalRequest) -> Option<&str> {
match request {
GuardianApprovalRequest::Shell { id, .. }
| GuardianApprovalRequest::ExecCommand { id, .. }
| GuardianApprovalRequest::ApplyPatch { id, .. }
| GuardianApprovalRequest::McpToolCall { id, .. }
| GuardianApprovalRequest::RequestPermissions { id, .. } => Some(id),
GuardianApprovalRequest::NetworkAccess { .. } => None,
#[cfg(unix)]
GuardianApprovalRequest::Execve { id, .. } => Some(id),
}
}
pub(crate) fn guardian_request_turn_id<'a>(
request: &'a GuardianApprovalRequest,
default_turn_id: &'a str,
) -> &'a str {
match request {
GuardianApprovalRequest::NetworkAccess { turn_id, .. }
| GuardianApprovalRequest::RequestPermissions { turn_id, .. } => turn_id,
GuardianApprovalRequest::Shell { .. }
| GuardianApprovalRequest::ExecCommand { .. }
| GuardianApprovalRequest::ApplyPatch { .. }
| GuardianApprovalRequest::McpToolCall { .. } => default_turn_id,
#[cfg(unix)]
GuardianApprovalRequest::Execve { .. } => default_turn_id,
}
}
pub(crate) fn format_guardian_action_pretty(
action: &GuardianApprovalRequest,
) -> serde_json::Result<FormattedGuardianAction> {
let value = guardian_approval_request_to_json(action)?;
let (value, truncated) = truncate_guardian_action_value(value);
Ok(FormattedGuardianAction {
text: serde_json::to_string_pretty(&value)?,
truncated,
})
}
pub(crate) use codex_guardian::format_guardian_action_pretty;
#[cfg(test)]
pub(crate) use codex_guardian::guardian_approval_request_to_json;
pub(crate) use codex_guardian::guardian_assessment_action;
pub(crate) use codex_guardian::guardian_request_target_item_id;
pub(crate) use codex_guardian::guardian_request_turn_id;
pub(crate) use codex_guardian::guardian_reviewed_action;

View File

@@ -60,9 +60,7 @@ const GUARDIAN_MAX_MESSAGE_TRANSCRIPT_TOKENS: usize = 10_000;
const GUARDIAN_MAX_TOOL_TRANSCRIPT_TOKENS: usize = 10_000;
const GUARDIAN_MAX_MESSAGE_ENTRY_TOKENS: usize = 2_000;
const GUARDIAN_MAX_TOOL_ENTRY_TOKENS: usize = 1_000;
const GUARDIAN_MAX_ACTION_STRING_TOKENS: usize = 16_000;
const GUARDIAN_RECENT_ENTRY_LIMIT: usize = 40;
const TRUNCATION_TAG: &str = "truncated";
/// Structured output contract that the guardian reviewer must satisfy.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]

View File

@@ -11,9 +11,7 @@ use crate::compact::content_items_to_text;
use crate::event_mapping::is_contextual_user_message_content;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use codex_utils_output_truncation::approx_bytes_for_tokens;
use codex_utils_output_truncation::approx_token_count;
use codex_utils_output_truncation::approx_tokens_from_byte_count;
use super::AUTO_REVIEW_DENIED_ACTION_APPROVAL_DEVELOPER_PREFIX;
use super::GUARDIAN_MAX_MESSAGE_ENTRY_TOKENS;
@@ -23,8 +21,8 @@ use super::GUARDIAN_MAX_TOOL_TRANSCRIPT_TOKENS;
use super::GUARDIAN_RECENT_ENTRY_LIMIT;
use super::GuardianApprovalRequest;
use super::GuardianAssessment;
use super::TRUNCATION_TAG;
use super::approval_request::format_guardian_action_pretty;
pub(crate) use codex_guardian::guardian_truncate_text;
/// Transcript entry retained for guardian review after filtering.
#[derive(Debug, PartialEq, Eq)]
@@ -513,68 +511,6 @@ pub(crate) fn collect_guardian_transcript_entries(
entries
}
pub(crate) fn guardian_truncate_text(content: &str, token_cap: usize) -> (String, bool) {
if content.is_empty() {
return (String::new(), false);
}
let max_bytes = approx_bytes_for_tokens(token_cap);
if content.len() <= max_bytes {
return (content.to_string(), false);
}
let omitted_tokens = approx_tokens_from_byte_count(content.len().saturating_sub(max_bytes));
let marker = format!("<{TRUNCATION_TAG} omitted_approx_tokens=\"{omitted_tokens}\" />");
if max_bytes <= marker.len() {
return (marker, true);
}
let available_bytes = max_bytes.saturating_sub(marker.len());
let prefix_budget = available_bytes / 2;
let suffix_budget = available_bytes.saturating_sub(prefix_budget);
let (prefix, suffix) = split_guardian_truncation_bounds(content, prefix_budget, suffix_budget);
(format!("{prefix}{marker}{suffix}"), true)
}
fn split_guardian_truncation_bounds(
content: &str,
prefix_bytes: usize,
suffix_bytes: usize,
) -> (&str, &str) {
if content.is_empty() {
return ("", "");
}
let len = content.len();
let suffix_start_target = len.saturating_sub(suffix_bytes);
let mut prefix_end = 0usize;
let mut suffix_start = len;
let mut suffix_started = false;
for (index, ch) in content.char_indices() {
let char_end = index + ch.len_utf8();
if char_end <= prefix_bytes {
prefix_end = char_end;
continue;
}
if index >= suffix_start_target {
if !suffix_started {
suffix_start = index;
suffix_started = true;
}
continue;
}
}
if suffix_start < prefix_end {
suffix_start = prefix_end;
}
(&content[..prefix_end], &content[suffix_start..])
}
/// The model is asked for strict JSON, but we still accept a surrounding prose
/// wrapper so transient formatting drift fails less noisily during dogfooding.
/// Non-JSON output is still a review failure; this is only a thin recovery path

View File

@@ -14,5 +14,11 @@ doctest = false
workspace = true
[dependencies]
codex-analytics = { workspace = true }
codex-extension-api = { workspace = true }
codex-protocol = { workspace = true }
codex-shell-command = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-output-truncation = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -0,0 +1,453 @@
use std::path::Path;
use codex_analytics::GuardianReviewedAction;
use codex_extension_api::ApprovalReviewMcpAnnotations as GuardianMcpAnnotations;
use codex_extension_api::ApprovalReviewNetworkAccessTrigger as GuardianNetworkAccessTrigger;
use codex_extension_api::ApprovalReviewRequest as GuardianApprovalRequest;
use codex_protocol::approvals::GuardianAssessmentAction;
use codex_protocol::approvals::GuardianCommandSource;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::SandboxPermissions;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Serialize;
use serde_json::Value;
use super::guardian_truncate_text;
const GUARDIAN_MAX_ACTION_STRING_TOKENS: usize = 16_000;
#[derive(Serialize)]
struct CommandApprovalAction<'a> {
tool: &'a str,
command: &'a [String],
cwd: &'a Path,
sandbox_permissions: SandboxPermissions,
#[serde(skip_serializing_if = "Option::is_none")]
additional_permissions: Option<&'a AdditionalPermissionProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
justification: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tty: Option<bool>,
}
#[cfg(unix)]
#[derive(Serialize)]
struct ExecveApprovalAction<'a> {
tool: &'a str,
program: &'a str,
argv: &'a [String],
cwd: &'a Path,
#[serde(skip_serializing_if = "Option::is_none")]
additional_permissions: Option<&'a AdditionalPermissionProfile>,
}
#[derive(Serialize)]
struct McpToolCallApprovalAction<'a> {
tool: &'static str,
server: &'a str,
tool_name: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
arguments: Option<&'a Value>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_id: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_name: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
connector_description: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_title: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_description: Option<&'a String>,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<&'a GuardianMcpAnnotations>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct NetworkAccessApprovalAction<'a> {
tool: &'static str,
target: &'a str,
host: &'a str,
protocol: NetworkApprovalProtocol,
port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
trigger: Option<&'a GuardianNetworkAccessTrigger>,
}
#[derive(Serialize)]
struct RequestPermissionsApprovalAction<'a> {
tool: &'static str,
turn_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<&'a String>,
permissions: &'a RequestPermissionProfile,
}
fn serialize_guardian_action(value: impl Serialize) -> serde_json::Result<Value> {
serde_json::to_value(value)
}
fn serialize_command_guardian_action(
tool: &'static str,
command: &[String],
cwd: &Path,
sandbox_permissions: SandboxPermissions,
additional_permissions: Option<&AdditionalPermissionProfile>,
justification: Option<&String>,
tty: Option<bool>,
) -> serde_json::Result<Value> {
serialize_guardian_action(CommandApprovalAction {
tool,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
tty,
})
}
fn command_assessment_action(
source: GuardianCommandSource,
command: &[String],
cwd: &AbsolutePathBuf,
) -> GuardianAssessmentAction {
GuardianAssessmentAction::Command {
source,
command: codex_shell_command::parse_command::shlex_join(command),
cwd: cwd.clone(),
}
}
#[cfg(unix)]
fn guardian_command_source_tool_name(source: GuardianCommandSource) -> &'static str {
match source {
GuardianCommandSource::Shell => "shell",
GuardianCommandSource::UnifiedExec => "exec_command",
}
}
fn truncate_guardian_action_value(value: Value) -> (Value, bool) {
match value {
Value::String(text) => {
let (text, truncated) =
guardian_truncate_text(&text, GUARDIAN_MAX_ACTION_STRING_TOKENS);
(Value::String(text), truncated)
}
Value::Array(values) => {
let mut truncated = false;
let values = values
.into_iter()
.map(|value| {
let (value, value_truncated) = truncate_guardian_action_value(value);
truncated |= value_truncated;
value
})
.collect::<Vec<_>>();
(Value::Array(values), truncated)
}
Value::Object(values) => {
let mut entries = values.into_iter().collect::<Vec<_>>();
entries.sort_by(|(left, _), (right, _)| left.cmp(right));
let mut truncated = false;
let values = entries
.into_iter()
.map(|(key, value)| {
let (value, value_truncated) = truncate_guardian_action_value(value);
truncated |= value_truncated;
(key, value)
})
.collect();
(Value::Object(values), truncated)
}
other => (other, false),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormattedGuardianAction {
pub text: String,
pub truncated: bool,
}
pub fn guardian_approval_request_to_json(
action: &GuardianApprovalRequest,
) -> serde_json::Result<Value> {
match action {
GuardianApprovalRequest::Shell {
id: _,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
} => serialize_command_guardian_action(
"shell",
command,
cwd,
*sandbox_permissions,
additional_permissions.as_ref(),
justification.as_ref(),
/*tty*/ None,
),
GuardianApprovalRequest::ExecCommand {
id: _,
command,
cwd,
sandbox_permissions,
additional_permissions,
justification,
tty,
} => serialize_command_guardian_action(
"exec_command",
command,
cwd,
*sandbox_permissions,
additional_permissions.as_ref(),
justification.as_ref(),
Some(*tty),
),
#[cfg(unix)]
GuardianApprovalRequest::Execve {
id: _,
source,
program,
argv,
cwd,
additional_permissions,
} => serialize_guardian_action(ExecveApprovalAction {
tool: guardian_command_source_tool_name(*source),
program,
argv,
cwd,
additional_permissions: additional_permissions.as_ref(),
}),
GuardianApprovalRequest::ApplyPatch {
id: _,
cwd,
files,
patch,
} => Ok(serde_json::json!({
"tool": "apply_patch",
"cwd": cwd,
"files": files,
"patch": patch,
})),
GuardianApprovalRequest::NetworkAccess {
id: _,
turn_id: _,
target,
host,
protocol,
port,
trigger,
} => serialize_guardian_action(NetworkAccessApprovalAction {
tool: "network_access",
target,
host,
protocol: *protocol,
port: *port,
trigger: trigger.as_ref(),
}),
GuardianApprovalRequest::McpToolCall {
id: _,
server,
tool_name,
arguments,
connector_id,
connector_name,
connector_description,
tool_title,
tool_description,
annotations,
} => serialize_guardian_action(McpToolCallApprovalAction {
tool: "mcp_tool_call",
server,
tool_name,
arguments: arguments.as_ref(),
connector_id: connector_id.as_ref(),
connector_name: connector_name.as_ref(),
connector_description: connector_description.as_ref(),
tool_title: tool_title.as_ref(),
tool_description: tool_description.as_ref(),
annotations: annotations.as_ref(),
}),
GuardianApprovalRequest::RequestPermissions {
id: _,
turn_id,
reason,
permissions,
} => serialize_guardian_action(RequestPermissionsApprovalAction {
tool: "request_permissions",
turn_id,
reason: reason.as_ref(),
permissions,
}),
}
}
pub fn guardian_assessment_action(action: &GuardianApprovalRequest) -> GuardianAssessmentAction {
match action {
GuardianApprovalRequest::Shell { command, cwd, .. } => {
command_assessment_action(GuardianCommandSource::Shell, command, cwd)
}
GuardianApprovalRequest::ExecCommand { command, cwd, .. } => {
command_assessment_action(GuardianCommandSource::UnifiedExec, command, cwd)
}
#[cfg(unix)]
GuardianApprovalRequest::Execve {
source,
program,
argv,
cwd,
..
} => GuardianAssessmentAction::Execve {
source: *source,
program: program.clone(),
argv: argv.clone(),
cwd: cwd.clone(),
},
GuardianApprovalRequest::ApplyPatch { cwd, files, .. } => {
GuardianAssessmentAction::ApplyPatch {
cwd: cwd.clone(),
files: files.clone(),
}
}
GuardianApprovalRequest::NetworkAccess {
id: _id,
turn_id: _turn_id,
target,
host,
protocol,
port,
trigger: _trigger,
} => GuardianAssessmentAction::NetworkAccess {
target: target.clone(),
host: host.clone(),
protocol: *protocol,
port: *port,
},
GuardianApprovalRequest::McpToolCall {
server,
tool_name,
connector_id,
connector_name,
tool_title,
..
} => GuardianAssessmentAction::McpToolCall {
server: server.clone(),
tool_name: tool_name.clone(),
connector_id: connector_id.clone(),
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions {
reason,
permissions,
..
} => GuardianAssessmentAction::RequestPermissions {
reason: reason.clone(),
permissions: permissions.clone(),
},
}
}
pub fn guardian_reviewed_action(request: &GuardianApprovalRequest) -> GuardianReviewedAction {
match request {
GuardianApprovalRequest::Shell {
sandbox_permissions,
additional_permissions,
..
} => GuardianReviewedAction::Shell {
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
},
GuardianApprovalRequest::ExecCommand {
sandbox_permissions,
additional_permissions,
tty,
..
} => GuardianReviewedAction::UnifiedExec {
sandbox_permissions: *sandbox_permissions,
additional_permissions: additional_permissions.clone(),
tty: *tty,
},
#[cfg(unix)]
GuardianApprovalRequest::Execve {
source,
program,
additional_permissions,
..
} => GuardianReviewedAction::Execve {
source: *source,
program: program.clone(),
additional_permissions: additional_permissions.clone(),
},
GuardianApprovalRequest::ApplyPatch { .. } => GuardianReviewedAction::ApplyPatch {},
GuardianApprovalRequest::NetworkAccess { protocol, port, .. } => {
GuardianReviewedAction::NetworkAccess {
protocol: *protocol,
port: *port,
}
}
GuardianApprovalRequest::McpToolCall {
server,
tool_name,
connector_id,
connector_name,
tool_title,
..
} => GuardianReviewedAction::McpToolCall {
server: server.clone(),
tool_name: tool_name.clone(),
connector_id: connector_id.clone(),
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions { .. } => {
GuardianReviewedAction::RequestPermissions {}
}
}
}
pub fn guardian_request_target_item_id(request: &GuardianApprovalRequest) -> Option<&str> {
match request {
GuardianApprovalRequest::Shell { id, .. }
| GuardianApprovalRequest::ExecCommand { id, .. }
| GuardianApprovalRequest::ApplyPatch { id, .. }
| GuardianApprovalRequest::McpToolCall { id, .. }
| GuardianApprovalRequest::RequestPermissions { id, .. } => Some(id),
GuardianApprovalRequest::NetworkAccess { .. } => None,
#[cfg(unix)]
GuardianApprovalRequest::Execve { id, .. } => Some(id),
}
}
pub fn guardian_request_turn_id<'a>(
request: &'a GuardianApprovalRequest,
default_turn_id: &'a str,
) -> &'a str {
match request {
GuardianApprovalRequest::NetworkAccess { turn_id, .. }
| GuardianApprovalRequest::RequestPermissions { turn_id, .. } => turn_id,
GuardianApprovalRequest::Shell { .. }
| GuardianApprovalRequest::ExecCommand { .. }
| GuardianApprovalRequest::ApplyPatch { .. }
| GuardianApprovalRequest::McpToolCall { .. } => default_turn_id,
#[cfg(unix)]
GuardianApprovalRequest::Execve { .. } => default_turn_id,
}
}
pub fn format_guardian_action_pretty(
action: &GuardianApprovalRequest,
) -> serde_json::Result<FormattedGuardianAction> {
let value = guardian_approval_request_to_json(action)?;
let (value, truncated) = truncate_guardian_action_value(value);
Ok(FormattedGuardianAction {
text: serde_json::to_string_pretty(&value)?,
truncated,
})
}

View File

@@ -13,6 +13,18 @@ use codex_protocol::ThreadId;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::protocol::AskForApproval;
mod approval_request;
mod truncation;
pub use approval_request::FormattedGuardianAction;
pub use approval_request::format_guardian_action_pretty;
pub use approval_request::guardian_approval_request_to_json;
pub use approval_request::guardian_assessment_action;
pub use approval_request::guardian_request_target_item_id;
pub use approval_request::guardian_request_turn_id;
pub use approval_request::guardian_reviewed_action;
pub use truncation::guardian_truncate_text;
/// Guardian extension dependencies supplied by the host at construction time.
#[derive(Clone, Debug)]
pub struct GuardianExtension<S> {

View File

@@ -0,0 +1,67 @@
use codex_utils_output_truncation::approx_bytes_for_tokens;
use codex_utils_output_truncation::approx_tokens_from_byte_count;
const TRUNCATION_TAG: &str = "truncated";
/// Truncates text to an approximate token cap while retaining its prefix and suffix.
pub fn guardian_truncate_text(content: &str, token_cap: usize) -> (String, bool) {
if content.is_empty() {
return (String::new(), false);
}
let max_bytes = approx_bytes_for_tokens(token_cap);
if content.len() <= max_bytes {
return (content.to_string(), false);
}
let omitted_tokens = approx_tokens_from_byte_count(content.len().saturating_sub(max_bytes));
let marker = format!("<{TRUNCATION_TAG} omitted_approx_tokens=\"{omitted_tokens}\" />");
if max_bytes <= marker.len() {
return (marker, true);
}
let available_bytes = max_bytes.saturating_sub(marker.len());
let prefix_budget = available_bytes / 2;
let suffix_budget = available_bytes.saturating_sub(prefix_budget);
let (prefix, suffix) = split_guardian_truncation_bounds(content, prefix_budget, suffix_budget);
(format!("{prefix}{marker}{suffix}"), true)
}
fn split_guardian_truncation_bounds(
content: &str,
prefix_bytes: usize,
suffix_bytes: usize,
) -> (&str, &str) {
if content.is_empty() {
return ("", "");
}
let len = content.len();
let suffix_start_target = len.saturating_sub(suffix_bytes);
let mut prefix_end = 0usize;
let mut suffix_start = len;
let mut suffix_started = false;
for (index, ch) in content.char_indices() {
let char_end = index + ch.len_utf8();
if char_end <= prefix_bytes {
prefix_end = char_end;
continue;
}
if index >= suffix_start_target {
if !suffix_started {
suffix_start = index;
suffix_started = true;
}
continue;
}
}
if suffix_start < prefix_end {
suffix_start = prefix_end;
}
(&content[..prefix_end], &content[suffix_start..])
}