mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
guardian: move approval request formatting out of core
This commit is contained in:
7
codex-rs/Cargo.lock
generated
7
codex-rs/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
453
codex-rs/ext/guardian/src/approval_request.rs
Normal file
453
codex-rs/ext/guardian/src/approval_request.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
67
codex-rs/ext/guardian/src/truncation.rs
Normal file
67
codex-rs/ext/guardian/src/truncation.rs
Normal 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..])
|
||||
}
|
||||
Reference in New Issue
Block a user