mirror of
https://github.com/openai/codex.git
synced 2026-05-07 04:47:13 +00:00
Compare commits
13 Commits
dev/efraze
...
abhinav/ty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a891c4d325 | ||
|
|
dbff525377 | ||
|
|
c30a4d2756 | ||
|
|
11655ee6b3 | ||
|
|
27c2bdc3e7 | ||
|
|
7113173442 | ||
|
|
49d55626f6 | ||
|
|
280698fcf6 | ||
|
|
a1c0d3e5b3 | ||
|
|
f06870f376 | ||
|
|
ad605dc791 | ||
|
|
fd6644fd82 | ||
|
|
15bb7f5530 |
976
codex-rs/core/src/approval_request.rs
Normal file
976
codex-rs/core/src/approval_request.rs
Normal file
@@ -0,0 +1,976 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_analytics::GuardianReviewedAction;
|
||||
use codex_protocol::approvals::ExecApprovalRequestEvent;
|
||||
use codex_protocol::approvals::GuardianAssessmentAction;
|
||||
use codex_protocol::approvals::GuardianCommandSource;
|
||||
use codex_protocol::approvals::NetworkApprovalContext;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::approvals::NetworkPolicyAmendment;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsEvent;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::guardian::GUARDIAN_MAX_ACTION_STRING_TOKENS;
|
||||
use crate::guardian::guardian_truncate_text;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
|
||||
/// Canonical description of an approval-worthy action in core.
|
||||
///
|
||||
/// This type should describe the action being reviewed exactly once, with
|
||||
/// guardian review, approval hooks, and user-prompt transports deriving their
|
||||
/// own projections from it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum ApprovalRequest {
|
||||
Command(CommandApprovalRequest),
|
||||
ApplyPatch(ApplyPatchApprovalRequest),
|
||||
McpToolCall(McpToolCallApprovalRequest),
|
||||
RequestPermissions(RequestPermissionsApprovalRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum CommandApprovalRequest {
|
||||
Shell {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
hook_command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
justification: Option<String>,
|
||||
},
|
||||
ExecCommand {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
hook_command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
justification: Option<String>,
|
||||
tty: bool,
|
||||
},
|
||||
#[cfg(unix)]
|
||||
Execve {
|
||||
id: String,
|
||||
source: GuardianCommandSource,
|
||||
program: String,
|
||||
argv: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
},
|
||||
NetworkAccess {
|
||||
id: String,
|
||||
turn_id: String,
|
||||
target: String,
|
||||
hook_command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
host: String,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
port: u16,
|
||||
trigger: Option<GuardianNetworkAccessTrigger>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ApplyPatchApprovalRequest {
|
||||
pub(crate) id: String,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) files: Vec<AbsolutePathBuf>,
|
||||
pub(crate) changes: HashMap<PathBuf, FileChange>,
|
||||
pub(crate) patch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct McpToolCallApprovalRequest {
|
||||
pub(crate) id: String,
|
||||
pub(crate) server: String,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) hook_tool_name: String,
|
||||
pub(crate) arguments: Option<Value>,
|
||||
pub(crate) connector_id: Option<String>,
|
||||
pub(crate) connector_name: Option<String>,
|
||||
pub(crate) connector_description: Option<String>,
|
||||
pub(crate) tool_title: Option<String>,
|
||||
pub(crate) tool_description: Option<String>,
|
||||
pub(crate) annotations: Option<GuardianMcpAnnotations>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct RequestPermissionsApprovalRequest {
|
||||
pub(crate) id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) reason: Option<String>,
|
||||
pub(crate) permissions: RequestPermissionProfile,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl ApprovalRequest {
|
||||
pub(crate) fn permission_request_payload(&self) -> Option<PermissionRequestPayload> {
|
||||
match self {
|
||||
Self::Command(request) => Some(request.permission_request_payload()),
|
||||
Self::ApplyPatch(request) => Some(request.permission_request_payload()),
|
||||
Self::McpToolCall(request) => Some(request.permission_request_payload()),
|
||||
Self::RequestPermissions(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CommandApprovalRequest {
|
||||
pub(crate) fn permission_request_payload(&self) -> PermissionRequestPayload {
|
||||
match self {
|
||||
Self::Shell {
|
||||
hook_command,
|
||||
justification,
|
||||
..
|
||||
}
|
||||
| Self::ExecCommand {
|
||||
hook_command,
|
||||
justification,
|
||||
..
|
||||
} => PermissionRequestPayload::bash(hook_command.clone(), justification.clone()),
|
||||
#[cfg(unix)]
|
||||
Self::Execve { program, argv, .. } => {
|
||||
let mut command = vec![program.clone()];
|
||||
if argv.len() > 1 {
|
||||
command.extend_from_slice(&argv[1..]);
|
||||
}
|
||||
PermissionRequestPayload::bash(
|
||||
codex_shell_command::parse_command::shlex_join(&command),
|
||||
/*description*/ None,
|
||||
)
|
||||
}
|
||||
Self::NetworkAccess {
|
||||
target,
|
||||
hook_command,
|
||||
..
|
||||
} => PermissionRequestPayload::bash(
|
||||
hook_command.clone(),
|
||||
Some(format!("network-access {target}")),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn exec_approval_event(
|
||||
&self,
|
||||
turn_id: String,
|
||||
approval_id: Option<String>,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
proposed_network_policy_amendments: Option<Vec<NetworkPolicyAmendment>>,
|
||||
available_decisions: Option<Vec<ReviewDecision>>,
|
||||
) -> ExecApprovalRequestEvent {
|
||||
match self {
|
||||
Self::Shell {
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
additional_permissions,
|
||||
..
|
||||
}
|
||||
| Self::ExecCommand {
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
additional_permissions,
|
||||
..
|
||||
} => ExecApprovalRequestEvent {
|
||||
call_id: id.clone(),
|
||||
approval_id,
|
||||
turn_id,
|
||||
command: command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
available_decisions,
|
||||
parsed_cmd: codex_shell_command::parse_command::parse_command(command),
|
||||
},
|
||||
#[cfg(unix)]
|
||||
Self::Execve {
|
||||
id,
|
||||
argv,
|
||||
cwd,
|
||||
additional_permissions,
|
||||
..
|
||||
} => ExecApprovalRequestEvent {
|
||||
call_id: id.clone(),
|
||||
approval_id,
|
||||
turn_id,
|
||||
command: argv.clone(),
|
||||
cwd: cwd.clone(),
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
available_decisions,
|
||||
parsed_cmd: codex_shell_command::parse_command::parse_command(argv),
|
||||
},
|
||||
Self::NetworkAccess {
|
||||
id,
|
||||
turn_id,
|
||||
target,
|
||||
cwd,
|
||||
host,
|
||||
protocol,
|
||||
..
|
||||
} => {
|
||||
let command = vec!["network-access".to_string(), target.clone()];
|
||||
let network_approval_context = Some(NetworkApprovalContext {
|
||||
host: host.clone(),
|
||||
protocol: *protocol,
|
||||
});
|
||||
let proposed_network_policy_amendments = proposed_network_policy_amendments
|
||||
.or_else(|| {
|
||||
Some(vec![
|
||||
NetworkPolicyAmendment {
|
||||
host: host.clone(),
|
||||
action: codex_protocol::approvals::NetworkPolicyRuleAction::Allow,
|
||||
},
|
||||
NetworkPolicyAmendment {
|
||||
host: host.clone(),
|
||||
action: codex_protocol::approvals::NetworkPolicyRuleAction::Deny,
|
||||
},
|
||||
])
|
||||
});
|
||||
ExecApprovalRequestEvent {
|
||||
call_id: id.clone(),
|
||||
approval_id,
|
||||
turn_id: turn_id.clone(),
|
||||
command: command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions: None,
|
||||
available_decisions,
|
||||
parsed_cmd: codex_shell_command::parse_command::parse_command(&command),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplyPatchApprovalRequest {
|
||||
pub(crate) fn permission_request_payload(&self) -> PermissionRequestPayload {
|
||||
PermissionRequestPayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
tool_input: serde_json::json!({ "command": self.patch }),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_patch_approval_event(
|
||||
&self,
|
||||
turn_id: String,
|
||||
reason: Option<String>,
|
||||
grant_root: Option<PathBuf>,
|
||||
) -> ApplyPatchApprovalRequestEvent {
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id: self.id.clone(),
|
||||
turn_id,
|
||||
changes: self.changes.clone(),
|
||||
reason,
|
||||
grant_root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl McpToolCallApprovalRequest {
|
||||
pub(crate) fn permission_request_payload(&self) -> PermissionRequestPayload {
|
||||
PermissionRequestPayload {
|
||||
tool_name: HookToolName::new(self.hook_tool_name.clone()),
|
||||
tool_input: self
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or_else(|| Value::Object(serde_json::Map::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequestPermissionsApprovalRequest {
|
||||
pub(crate) fn request_permissions_event(&self) -> RequestPermissionsEvent {
|
||||
RequestPermissionsEvent {
|
||||
call_id: self.id.clone(),
|
||||
turn_id: self.turn_id.clone(),
|
||||
reason: self.reason.clone(),
|
||||
permissions: self.permissions.clone(),
|
||||
cwd: Some(self.cwd.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandApprovalRequest> for ApprovalRequest {
|
||||
fn from(value: CommandApprovalRequest) -> Self {
|
||||
Self::Command(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ApplyPatchApprovalRequest> for ApprovalRequest {
|
||||
fn from(value: ApplyPatchApprovalRequest) -> Self {
|
||||
Self::ApplyPatch(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<McpToolCallApprovalRequest> for ApprovalRequest {
|
||||
fn from(value: McpToolCallApprovalRequest) -> Self {
|
||||
Self::McpToolCall(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestPermissionsApprovalRequest> for ApprovalRequest {
|
||||
fn from(value: RequestPermissionsApprovalRequest) -> Self {
|
||||
Self::RequestPermissions(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GuardianNetworkAccessTrigger {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) justification: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) tty: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub(crate) struct GuardianMcpAnnotations {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) destructive_hint: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) open_world_hint: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) read_only_hint: Option<bool>,
|
||||
}
|
||||
|
||||
#[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: &ApprovalRequest,
|
||||
) -> serde_json::Result<Value> {
|
||||
match action {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::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,
|
||||
),
|
||||
ApprovalRequest::Command(CommandApprovalRequest::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)]
|
||||
ApprovalRequest::Command(CommandApprovalRequest::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(),
|
||||
}),
|
||||
ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: _,
|
||||
cwd,
|
||||
files,
|
||||
changes: _,
|
||||
patch,
|
||||
}) => Ok(serde_json::json!({
|
||||
"tool": "apply_patch",
|
||||
"cwd": cwd,
|
||||
"files": files,
|
||||
"patch": patch,
|
||||
})),
|
||||
ApprovalRequest::Command(CommandApprovalRequest::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(),
|
||||
}),
|
||||
ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
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(),
|
||||
}),
|
||||
ApprovalRequest::RequestPermissions(RequestPermissionsApprovalRequest {
|
||||
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: &ApprovalRequest) -> GuardianAssessmentAction {
|
||||
match action {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell { command, cwd, .. }) => {
|
||||
command_assessment_action(GuardianCommandSource::Shell, command, cwd)
|
||||
}
|
||||
ApprovalRequest::Command(CommandApprovalRequest::ExecCommand { command, cwd, .. }) => {
|
||||
command_assessment_action(GuardianCommandSource::UnifiedExec, command, cwd)
|
||||
}
|
||||
#[cfg(unix)]
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Execve {
|
||||
source,
|
||||
program,
|
||||
argv,
|
||||
cwd,
|
||||
..
|
||||
}) => GuardianAssessmentAction::Execve {
|
||||
source: *source,
|
||||
program: program.clone(),
|
||||
argv: argv.clone(),
|
||||
cwd: cwd.clone(),
|
||||
},
|
||||
ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest { cwd, files, .. }) => {
|
||||
GuardianAssessmentAction::ApplyPatch {
|
||||
cwd: cwd.clone(),
|
||||
files: files.clone(),
|
||||
}
|
||||
}
|
||||
ApprovalRequest::Command(CommandApprovalRequest::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,
|
||||
},
|
||||
ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
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(),
|
||||
},
|
||||
ApprovalRequest::RequestPermissions(RequestPermissionsApprovalRequest {
|
||||
reason,
|
||||
permissions,
|
||||
..
|
||||
}) => GuardianAssessmentAction::RequestPermissions {
|
||||
reason: reason.clone(),
|
||||
permissions: permissions.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn guardian_reviewed_action(request: &ApprovalRequest) -> GuardianReviewedAction {
|
||||
match request {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
sandbox_permissions,
|
||||
additional_permissions,
|
||||
..
|
||||
}) => GuardianReviewedAction::Shell {
|
||||
sandbox_permissions: *sandbox_permissions,
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
},
|
||||
ApprovalRequest::Command(CommandApprovalRequest::ExecCommand {
|
||||
sandbox_permissions,
|
||||
additional_permissions,
|
||||
tty,
|
||||
..
|
||||
}) => GuardianReviewedAction::UnifiedExec {
|
||||
sandbox_permissions: *sandbox_permissions,
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
tty: *tty,
|
||||
},
|
||||
#[cfg(unix)]
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Execve {
|
||||
source,
|
||||
program,
|
||||
additional_permissions,
|
||||
..
|
||||
}) => GuardianReviewedAction::Execve {
|
||||
source: *source,
|
||||
program: program.clone(),
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
},
|
||||
ApprovalRequest::ApplyPatch(..) => GuardianReviewedAction::ApplyPatch {},
|
||||
ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess {
|
||||
protocol, port, ..
|
||||
}) => GuardianReviewedAction::NetworkAccess {
|
||||
protocol: *protocol,
|
||||
port: *port,
|
||||
},
|
||||
ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
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(),
|
||||
},
|
||||
ApprovalRequest::RequestPermissions(..) => GuardianReviewedAction::RequestPermissions {},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn guardian_request_target_item_id(request: &ApprovalRequest) -> Option<&str> {
|
||||
match request {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell { id, .. })
|
||||
| ApprovalRequest::Command(CommandApprovalRequest::ExecCommand { id, .. })
|
||||
| ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest { id, .. })
|
||||
| ApprovalRequest::McpToolCall(McpToolCallApprovalRequest { id, .. })
|
||||
| ApprovalRequest::RequestPermissions(RequestPermissionsApprovalRequest { id, .. }) => {
|
||||
Some(id)
|
||||
}
|
||||
ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess { .. }) => None,
|
||||
#[cfg(unix)]
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Execve { id, .. }) => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn guardian_request_turn_id<'a>(
|
||||
request: &'a ApprovalRequest,
|
||||
default_turn_id: &'a str,
|
||||
) -> &'a str {
|
||||
match request {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess { turn_id, .. })
|
||||
| ApprovalRequest::RequestPermissions(RequestPermissionsApprovalRequest {
|
||||
turn_id, ..
|
||||
}) => turn_id,
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell { .. })
|
||||
| ApprovalRequest::Command(CommandApprovalRequest::ExecCommand { .. })
|
||||
| ApprovalRequest::ApplyPatch(..)
|
||||
| ApprovalRequest::McpToolCall(..) => default_turn_id,
|
||||
#[cfg(unix)]
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Execve { .. }) => default_turn_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_guardian_action_pretty(
|
||||
action: &ApprovalRequest,
|
||||
) -> 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,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn exec_approval_event_is_projected_from_shell_request() {
|
||||
let request = CommandApprovalRequest::Shell {
|
||||
id: "call-1".to_string(),
|
||||
command: vec!["echo".to_string(), "hi".to_string()],
|
||||
hook_command: "echo hi".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("because".to_string()),
|
||||
};
|
||||
|
||||
let event = request.exec_approval_event(
|
||||
"turn-1".to_string(),
|
||||
Some("approval-1".to_string()),
|
||||
Some("retry".to_string()),
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
/*proposed_network_policy_amendments*/ None,
|
||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
||||
);
|
||||
|
||||
assert_eq!(event.call_id, "call-1");
|
||||
assert_eq!(event.approval_id.as_deref(), Some("approval-1"));
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert_eq!(event.command, vec!["echo".to_string(), "hi".to_string()]);
|
||||
assert_eq!(event.reason.as_deref(), Some("retry"));
|
||||
assert_eq!(
|
||||
event.available_decisions,
|
||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_patch_approval_event_is_projected_from_request() {
|
||||
let path = test_path_buf("/tmp/file.txt");
|
||||
let abs_path = path.abs();
|
||||
let request = ApplyPatchApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
files: vec![abs_path],
|
||||
changes: HashMap::from([(
|
||||
path.clone(),
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)]),
|
||||
patch: "*** Begin Patch".to_string(),
|
||||
};
|
||||
|
||||
let event = request.apply_patch_approval_event(
|
||||
"turn-1".to_string(),
|
||||
Some("needs write".to_string()),
|
||||
/*grant_root*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(event.call_id, "call-1");
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert_eq!(event.reason.as_deref(), Some("needs write"));
|
||||
assert_eq!(
|
||||
event.changes,
|
||||
HashMap::from([(
|
||||
path,
|
||||
FileChange::Add {
|
||||
content: "hello".to_string(),
|
||||
},
|
||||
)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_permissions_event_is_projected_from_request() {
|
||||
let request = RequestPermissionsApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
reason: Some("need outbound network".to_string()),
|
||||
permissions: RequestPermissionProfile {
|
||||
network: Some(codex_protocol::models::NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
},
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
};
|
||||
|
||||
let event = request.request_permissions_event();
|
||||
|
||||
assert_eq!(event.call_id, "call-1");
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert_eq!(event.reason.as_deref(), Some("need outbound network"));
|
||||
assert_eq!(
|
||||
event.permissions,
|
||||
RequestPermissionProfile {
|
||||
network: Some(codex_protocol::models::NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(event.cwd, Some(test_path_buf("/tmp").abs()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_exec_approval_event_is_projected_from_request() {
|
||||
let request = CommandApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
target: "https://example.com:443".to_string(),
|
||||
hook_command: "curl https://example.com".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
trigger: None,
|
||||
};
|
||||
|
||||
let event = request.exec_approval_event(
|
||||
"ignored-turn".to_string(),
|
||||
/*approval_id*/ None,
|
||||
Some("need network".to_string()),
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
/*proposed_network_policy_amendments*/ None,
|
||||
/*available_decisions*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(event.call_id, "network-1");
|
||||
assert_eq!(event.turn_id, "turn-1");
|
||||
assert_eq!(
|
||||
event.command,
|
||||
vec![
|
||||
"network-access".to_string(),
|
||||
"https://example.com:443".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(event.cwd, test_path_buf("/tmp").abs());
|
||||
assert_eq!(event.reason.as_deref(), Some("need network"));
|
||||
assert_eq!(
|
||||
event.network_approval_context,
|
||||
Some(NetworkApprovalContext {
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
event.proposed_network_policy_amendments,
|
||||
Some(vec![
|
||||
NetworkPolicyAmendment {
|
||||
host: "example.com".to_string(),
|
||||
action: codex_protocol::approvals::NetworkPolicyRuleAction::Allow,
|
||||
},
|
||||
NetworkPolicyAmendment {
|
||||
host: "example.com".to_string(),
|
||||
action: codex_protocol::approvals::NetworkPolicyRuleAction::Deny,
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RequestUserInputEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::Submission;
|
||||
@@ -23,24 +22,24 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::approval_request::ApplyPatchApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::config::Config;
|
||||
use crate::environment_selection::ResolvedTurnEnvironments;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::guardian::spawn_approval_request_review;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
|
||||
use crate::mcp_tool_call::build_guardian_mcp_tool_review_request;
|
||||
use crate::mcp_tool_call::build_mcp_tool_approval_request;
|
||||
use crate::mcp_tool_call::is_mcp_tool_approval_question_id;
|
||||
use crate::mcp_tool_call::lookup_mcp_tool_metadata;
|
||||
use crate::mcp_tool_call::mcp_tool_approval_compat_response;
|
||||
use crate::session::Codex;
|
||||
use crate::session::CodexSpawnArgs;
|
||||
use crate::session::CodexSpawnOk;
|
||||
@@ -452,25 +451,28 @@ async fn handle_exec_approval(
|
||||
available_decisions,
|
||||
..
|
||||
} = event;
|
||||
let hook_command = shlex_join(&command);
|
||||
let approval_request = CommandApprovalRequest::Shell {
|
||||
id: call_id.clone(),
|
||||
command,
|
||||
hook_command,
|
||||
cwd,
|
||||
sandbox_permissions: if additional_permissions.is_some() {
|
||||
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions
|
||||
} else {
|
||||
crate::sandboxing::SandboxPermissions::UseDefault
|
||||
},
|
||||
additional_permissions,
|
||||
justification: None,
|
||||
};
|
||||
let decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let review_rx = spawn_approval_request_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: call_id.clone(),
|
||||
command,
|
||||
cwd,
|
||||
sandbox_permissions: if additional_permissions.is_some() {
|
||||
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions
|
||||
} else {
|
||||
crate::sandboxing::SandboxPermissions::UseDefault
|
||||
},
|
||||
additional_permissions,
|
||||
justification: None,
|
||||
},
|
||||
reason,
|
||||
approval_request.clone().into(),
|
||||
reason.clone(),
|
||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
||||
review_cancel.clone(),
|
||||
);
|
||||
@@ -484,16 +486,13 @@ async fn handle_exec_approval(
|
||||
.await
|
||||
} else {
|
||||
await_approval_with_cancel(
|
||||
parent_session.request_command_approval(
|
||||
parent_session.request_command_approval_for_request(
|
||||
parent_ctx,
|
||||
call_id,
|
||||
approval_request,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
available_decisions,
|
||||
),
|
||||
parent_session,
|
||||
@@ -530,49 +529,50 @@ async fn handle_patch_approval(
|
||||
..
|
||||
} = event;
|
||||
let approval_id = call_id.clone();
|
||||
let guardian_decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let files = changes
|
||||
let patch = changes
|
||||
.iter()
|
||||
.map(|(path, change)| match change {
|
||||
codex_protocol::protocol::FileChange::Add { content } => {
|
||||
format!("*** Add File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Delete { content } => {
|
||||
format!("*** Delete File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(move_path) = move_path {
|
||||
format!(
|
||||
"*** Update File: {}\n*** Move to: {}\n{}",
|
||||
path.display(),
|
||||
move_path.display(),
|
||||
unified_diff
|
||||
)
|
||||
} else {
|
||||
format!("*** Update File: {}\n{}", path.display(), unified_diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let approval_request = ApplyPatchApprovalRequest {
|
||||
id: approval_id.clone(),
|
||||
cwd: parent_ctx.cwd.clone(),
|
||||
files: changes
|
||||
.keys()
|
||||
.map(|path| parent_ctx.cwd.join(path))
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>(),
|
||||
changes: changes.clone(),
|
||||
patch,
|
||||
};
|
||||
let guardian_decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let patch = changes
|
||||
.iter()
|
||||
.map(|(path, change)| match change {
|
||||
codex_protocol::protocol::FileChange::Add { content } => {
|
||||
format!("*** Add File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Delete { content } => {
|
||||
format!("*** Delete File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(move_path) = move_path {
|
||||
format!(
|
||||
"*** Update File: {}\n*** Move to: {}\n{}",
|
||||
path.display(),
|
||||
move_path.display(),
|
||||
unified_diff
|
||||
)
|
||||
} else {
|
||||
format!("*** Update File: {}\n{}", path.display(), unified_diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let review_rx = spawn_approval_request_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: approval_id.clone(),
|
||||
cwd: parent_ctx.cwd.clone(),
|
||||
files,
|
||||
patch,
|
||||
},
|
||||
approval_request.clone().into(),
|
||||
reason.clone(),
|
||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
||||
review_cancel.clone(),
|
||||
@@ -594,7 +594,7 @@ async fn handle_patch_approval(
|
||||
decision
|
||||
} else {
|
||||
let decision_rx = parent_session
|
||||
.request_patch_approval(parent_ctx, call_id, changes, reason, grant_root)
|
||||
.request_patch_approval_for_request(parent_ctx, approval_request, reason, grant_root)
|
||||
.await;
|
||||
await_approval_with_cancel(
|
||||
async move { decision_rx.await.unwrap_or_default() },
|
||||
@@ -684,12 +684,18 @@ async fn maybe_auto_review_mcp_request_user_input(
|
||||
&invocation.tool,
|
||||
)
|
||||
.await;
|
||||
let approval_request = build_mcp_tool_approval_request(
|
||||
&event.call_id,
|
||||
&invocation.tool,
|
||||
&invocation,
|
||||
metadata.as_ref(),
|
||||
);
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let review_rx = spawn_approval_request_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
new_guardian_review_id(),
|
||||
build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()),
|
||||
approval_request.clone(),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequestSource::DelegatedSubagent,
|
||||
review_cancel.clone(),
|
||||
@@ -702,32 +708,7 @@ async fn maybe_auto_review_mcp_request_user_input(
|
||||
Some(&review_cancel),
|
||||
)
|
||||
.await;
|
||||
let selected_label = match decision {
|
||||
ReviewDecision::ApprovedForSession => question
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| {
|
||||
options
|
||||
.iter()
|
||||
.find(|option| option.label == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)
|
||||
})
|
||||
.map(|option| option.label.clone())
|
||||
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
|
||||
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
|
||||
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
|
||||
}
|
||||
};
|
||||
Some(RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
question.id.clone(),
|
||||
codex_protocol::request_user_input::RequestUserInputAnswer {
|
||||
answers: vec![selected_label],
|
||||
},
|
||||
)]),
|
||||
})
|
||||
mcp_tool_approval_compat_response(&approval_request, question, decision)
|
||||
}
|
||||
|
||||
async fn handle_request_permissions(
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use super::*;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX;
|
||||
use async_channel::bounded;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::AgentStatus;
|
||||
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::GuardianAssessmentAction;
|
||||
use codex_protocol::protocol::GuardianAssessmentStatus;
|
||||
use codex_protocol::protocol::GuardianCommandSource;
|
||||
@@ -384,6 +385,91 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handle_patch_approval_uses_tool_call_id_for_round_trip() {
|
||||
let (parent_session, parent_ctx, rx_events) =
|
||||
crate::session::tests::make_session_and_context_with_rx().await;
|
||||
*parent_session.active_turn.lock().await = Some(crate::state::ActiveTurn::default());
|
||||
|
||||
let (tx_sub, rx_sub) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_tx_events, rx_events_child) = bounded(SUBMISSION_CHANNEL_CAPACITY);
|
||||
let (_agent_status_tx, agent_status) = watch::channel(AgentStatus::PendingInit);
|
||||
let codex = Arc::new(Codex {
|
||||
tx_sub,
|
||||
rx_event: rx_events_child,
|
||||
agent_status,
|
||||
session: Arc::clone(&parent_session),
|
||||
session_loop_termination: completed_session_loop_termination(),
|
||||
});
|
||||
|
||||
let call_id = "patch-call-1".to_string();
|
||||
let changes = HashMap::from([(
|
||||
std::path::PathBuf::from("file.txt"),
|
||||
FileChange::Update {
|
||||
unified_diff: "@@ -1 +1 @@\n-old\n+new\n".to_string(),
|
||||
move_path: None,
|
||||
},
|
||||
)]);
|
||||
let expected_changes = changes.clone();
|
||||
let cancel_token = CancellationToken::new();
|
||||
|
||||
let handle = tokio::spawn({
|
||||
let codex = Arc::clone(&codex);
|
||||
let parent_session = Arc::clone(&parent_session);
|
||||
let parent_ctx = Arc::clone(&parent_ctx);
|
||||
let cancel_token = cancel_token.clone();
|
||||
let call_id = call_id.clone();
|
||||
async move {
|
||||
handle_patch_approval(
|
||||
codex.as_ref(),
|
||||
"child-turn-1".to_string(),
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
ApplyPatchApprovalRequestEvent {
|
||||
call_id,
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
changes,
|
||||
reason: Some("needs write".to_string()),
|
||||
grant_root: None,
|
||||
},
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let request_event = timeout(Duration::from_secs(1), rx_events.recv())
|
||||
.await
|
||||
.expect("patch approval event timed out")
|
||||
.expect("patch approval event missing");
|
||||
let EventMsg::ApplyPatchApprovalRequest(request) = request_event.msg else {
|
||||
panic!("expected ApplyPatchApprovalRequest event");
|
||||
};
|
||||
assert_eq!(request.call_id, call_id.clone());
|
||||
assert_eq!(request.changes, expected_changes);
|
||||
|
||||
parent_session
|
||||
.notify_approval(&call_id, ReviewDecision::Approved)
|
||||
.await;
|
||||
|
||||
timeout(Duration::from_secs(1), handle)
|
||||
.await
|
||||
.expect("handle_patch_approval hung")
|
||||
.expect("handle_patch_approval join error");
|
||||
|
||||
let submission = timeout(Duration::from_secs(1), rx_sub.recv())
|
||||
.await
|
||||
.expect("patch approval response timed out")
|
||||
.expect("patch approval response missing");
|
||||
assert_eq!(
|
||||
submission.op,
|
||||
Op::PatchApproval {
|
||||
id: call_id,
|
||||
decision: ReviewDecision::Approved,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
let (parent_session, parent_ctx, _rx_events) =
|
||||
@@ -417,7 +503,7 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
call_id: "call-1".to_string(),
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
questions: vec![RequestUserInputQuestion {
|
||||
id: format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
id: "mcp_tool_call_approval_call-1".to_string(),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
is_other: false,
|
||||
@@ -433,7 +519,7 @@ async fn delegated_mcp_guardian_abort_returns_synthetic_decline_answer() {
|
||||
response,
|
||||
Some(RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1"),
|
||||
"mcp_tool_call_approval_call-1".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec![MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()],
|
||||
},
|
||||
|
||||
@@ -1,541 +0,0 @@
|
||||
use std::path::Path;
|
||||
|
||||
use codex_analytics::GuardianReviewedAction;
|
||||
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(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum GuardianApprovalRequest {
|
||||
Shell {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
justification: Option<String>,
|
||||
},
|
||||
ExecCommand {
|
||||
id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
justification: Option<String>,
|
||||
tty: bool,
|
||||
},
|
||||
#[cfg(unix)]
|
||||
Execve {
|
||||
id: String,
|
||||
source: GuardianCommandSource,
|
||||
program: String,
|
||||
argv: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
},
|
||||
ApplyPatch {
|
||||
id: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
files: Vec<AbsolutePathBuf>,
|
||||
patch: String,
|
||||
},
|
||||
NetworkAccess {
|
||||
id: String,
|
||||
turn_id: String,
|
||||
target: String,
|
||||
host: String,
|
||||
protocol: NetworkApprovalProtocol,
|
||||
port: u16,
|
||||
trigger: Option<GuardianNetworkAccessTrigger>,
|
||||
},
|
||||
McpToolCall {
|
||||
id: String,
|
||||
server: String,
|
||||
tool_name: String,
|
||||
arguments: Option<Value>,
|
||||
connector_id: Option<String>,
|
||||
connector_name: Option<String>,
|
||||
connector_description: Option<String>,
|
||||
tool_title: Option<String>,
|
||||
tool_description: Option<String>,
|
||||
annotations: Option<GuardianMcpAnnotations>,
|
||||
},
|
||||
RequestPermissions {
|
||||
id: String,
|
||||
turn_id: String,
|
||||
reason: Option<String>,
|
||||
permissions: RequestPermissionProfile,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct GuardianNetworkAccessTrigger {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) tool_name: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) sandbox_permissions: crate::sandboxing::SandboxPermissions,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) justification: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) tty: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub(crate) struct GuardianMcpAnnotations {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) destructive_hint: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) open_world_hint: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) read_only_hint: Option<bool>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
})
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
//! 3. Fail closed on timeout, execution failure, or malformed output.
|
||||
//! 4. Apply the guardian's explicit allow/deny outcome.
|
||||
|
||||
mod approval_request;
|
||||
mod prompt;
|
||||
mod review;
|
||||
mod review_session;
|
||||
@@ -23,10 +22,7 @@ use codex_protocol::protocol::GuardianAssessmentOutcome;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub(crate) use approval_request::GuardianApprovalRequest;
|
||||
pub(crate) use approval_request::GuardianMcpAnnotations;
|
||||
pub(crate) use approval_request::GuardianNetworkAccessTrigger;
|
||||
pub(crate) use approval_request::guardian_approval_request_to_json;
|
||||
pub(crate) use crate::approval_request::guardian_approval_request_to_json;
|
||||
pub(crate) use review::guardian_rejection_message;
|
||||
pub(crate) use review::guardian_timeout_message;
|
||||
pub(crate) use review::is_guardian_reviewer_source;
|
||||
@@ -51,7 +47,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;
|
||||
pub(crate) const GUARDIAN_MAX_ACTION_STRING_TOKENS: usize = 16_000;
|
||||
const GUARDIAN_RECENT_ENTRY_LIMIT: usize = 40;
|
||||
const TRUNCATION_TAG: &str = "truncated";
|
||||
|
||||
@@ -121,11 +117,11 @@ impl GuardianRejectionCircuitBreaker {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
use approval_request::format_guardian_action_pretty;
|
||||
use crate::approval_request::format_guardian_action_pretty;
|
||||
#[cfg(test)]
|
||||
use approval_request::guardian_assessment_action;
|
||||
use crate::approval_request::guardian_assessment_action;
|
||||
#[cfg(test)]
|
||||
use approval_request::guardian_request_turn_id;
|
||||
use crate::approval_request::guardian_request_turn_id;
|
||||
#[cfg(test)]
|
||||
use prompt::GuardianPromptMode;
|
||||
#[cfg(test)]
|
||||
@@ -144,8 +140,7 @@ use prompt::guardian_output_schema;
|
||||
pub(crate) use prompt::guardian_policy_prompt;
|
||||
#[cfg(test)]
|
||||
pub(crate) use prompt::guardian_policy_prompt_with_config;
|
||||
#[cfg(test)]
|
||||
use prompt::guardian_truncate_text;
|
||||
pub(crate) use prompt::guardian_truncate_text;
|
||||
#[cfg(test)]
|
||||
use prompt::parse_guardian_assessment;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -20,10 +20,11 @@ use super::GUARDIAN_MAX_MESSAGE_TRANSCRIPT_TOKENS;
|
||||
use super::GUARDIAN_MAX_TOOL_ENTRY_TOKENS;
|
||||
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;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::format_guardian_action_pretty;
|
||||
|
||||
/// Transcript entry retained for guardian review after filtering.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
@@ -89,7 +90,7 @@ pub(crate) enum GuardianPromptMode {
|
||||
pub(crate) async fn build_guardian_prompt_items(
|
||||
session: &Session,
|
||||
retry_reason: Option<String>,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
mode: GuardianPromptMode,
|
||||
) -> serde_json::Result<GuardianPromptItems> {
|
||||
let history = session.clone_history().await;
|
||||
@@ -173,7 +174,7 @@ pub(crate) async fn build_guardian_prompt_items(
|
||||
push_text(format!("\n{note}\n"));
|
||||
}
|
||||
match &request {
|
||||
GuardianApprovalRequest::NetworkAccess { trigger, .. } => {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess { trigger, .. }) => {
|
||||
push_text(">>> APPROVAL REQUEST START\n".to_string());
|
||||
push_text("Below is a proposed network access request under review.\n".to_string());
|
||||
if trigger.is_some() {
|
||||
|
||||
@@ -26,20 +26,20 @@ use crate::session::turn_context::TurnContext;
|
||||
|
||||
use super::GUARDIAN_REVIEW_TIMEOUT;
|
||||
use super::GUARDIAN_REVIEWER_NAME;
|
||||
use super::GuardianApprovalRequest;
|
||||
use super::GuardianAssessment;
|
||||
use super::GuardianAssessmentOutcome;
|
||||
use super::GuardianRejection;
|
||||
use super::GuardianRejectionCircuitBreakerAction;
|
||||
use super::approval_request::guardian_assessment_action;
|
||||
use super::approval_request::guardian_request_target_item_id;
|
||||
use super::approval_request::guardian_request_turn_id;
|
||||
use super::approval_request::guardian_reviewed_action;
|
||||
use super::prompt::guardian_output_schema;
|
||||
use super::prompt::parse_guardian_assessment;
|
||||
use super::review_session::GuardianReviewSessionOutcome;
|
||||
use super::review_session::GuardianReviewSessionParams;
|
||||
use super::review_session::build_guardian_review_session_config;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::guardian_assessment_action;
|
||||
use crate::approval_request::guardian_request_target_item_id;
|
||||
use crate::approval_request::guardian_request_turn_id;
|
||||
use crate::approval_request::guardian_reviewed_action;
|
||||
|
||||
const GUARDIAN_REJECTION_INSTRUCTIONS: &str = concat!(
|
||||
"The agent must not attempt to achieve the same outcome via workaround, ",
|
||||
@@ -234,7 +234,7 @@ async fn run_guardian_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
approval_request_source: GuardianApprovalRequestSource,
|
||||
external_cancel: Option<CancellationToken>,
|
||||
@@ -523,7 +523,7 @@ pub(crate) async fn review_approval_request(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
) -> ReviewDecision {
|
||||
// Box the delegated review future so callers do not inline the entire
|
||||
@@ -544,7 +544,7 @@ pub(crate) async fn review_approval_request_with_cancel(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
approval_request_source: GuardianApprovalRequestSource,
|
||||
cancel_token: CancellationToken,
|
||||
@@ -565,7 +565,7 @@ pub(crate) fn spawn_approval_request_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
review_id: String,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
approval_request_source: GuardianApprovalRequestSource,
|
||||
cancel_token: CancellationToken,
|
||||
@@ -610,7 +610,7 @@ pub(crate) fn spawn_approval_request_review(
|
||||
pub(super) async fn run_guardian_review_session(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
request: GuardianApprovalRequest,
|
||||
request: ApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
schema: serde_json::Value,
|
||||
external_cancel: Option<CancellationToken>,
|
||||
|
||||
@@ -46,12 +46,14 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::GUARDIAN_REVIEW_TIMEOUT;
|
||||
use super::GUARDIAN_REVIEWER_NAME;
|
||||
use super::GuardianApprovalRequest;
|
||||
use super::prompt::GuardianPromptMode;
|
||||
use super::prompt::GuardianTranscriptCursor;
|
||||
use super::prompt::build_guardian_prompt_items;
|
||||
use super::prompt::guardian_policy_prompt;
|
||||
use super::prompt::guardian_policy_prompt_with_config;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
#[cfg(test)]
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
|
||||
const GUARDIAN_INTERRUPT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
#[derive(Debug)]
|
||||
@@ -67,7 +69,7 @@ pub(crate) struct GuardianReviewSessionParams {
|
||||
pub(crate) parent_session: Arc<Session>,
|
||||
pub(crate) parent_turn: Arc<TurnContext>,
|
||||
pub(crate) spawn_config: Config,
|
||||
pub(crate) request: GuardianApprovalRequest,
|
||||
pub(crate) request: ApprovalRequest,
|
||||
pub(crate) retry_reason: Option<String>,
|
||||
pub(crate) schema: Value,
|
||||
pub(crate) model: String,
|
||||
@@ -1101,14 +1103,15 @@ mod tests {
|
||||
parent_session: Arc::new(session),
|
||||
parent_turn: Arc::new(turn),
|
||||
spawn_config,
|
||||
request: GuardianApprovalRequest::Shell {
|
||||
request: ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
hook_command: "git status".to_string(),
|
||||
cwd,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Inspect repo state.".to_string()),
|
||||
},
|
||||
}),
|
||||
retry_reason: None,
|
||||
schema: super::super::prompt::guardian_output_schema(),
|
||||
model,
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
use crate::approval_request::ApplyPatchApprovalRequest;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::GuardianMcpAnnotations;
|
||||
use crate::approval_request::GuardianNetworkAccessTrigger;
|
||||
use crate::approval_request::McpToolCallApprovalRequest;
|
||||
use crate::approval_request::guardian_request_target_item_id;
|
||||
use crate::config::Config;
|
||||
use crate::config::ConfigOverrides;
|
||||
use crate::config::Constrained;
|
||||
use crate::config::ManagedFeatures;
|
||||
use crate::config::NetworkProxySpec;
|
||||
use crate::config::test_config;
|
||||
use crate::guardian::approval_request::guardian_request_target_item_id;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::test_support;
|
||||
@@ -60,7 +68,6 @@ use insta::Settings;
|
||||
use insta::assert_snapshot;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
@@ -312,14 +319,15 @@ async fn build_guardian_prompt_full_mode_preserves_initial_review_format() -> an
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
Some("Sandbox denied outbound git push to github.com.".to_string()),
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the reviewed docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Full,
|
||||
)
|
||||
.await?;
|
||||
@@ -366,14 +374,15 @@ async fn build_guardian_prompt_delta_mode_preserves_original_numbering() -> anyh
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-2".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the second docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Delta {
|
||||
cursor: GuardianTranscriptCursor {
|
||||
parent_history_version: 0,
|
||||
@@ -404,14 +413,15 @@ async fn build_guardian_prompt_delta_mode_handles_empty_delta() -> anyhow::Resul
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-2".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the second docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Delta {
|
||||
cursor: GuardianTranscriptCursor {
|
||||
parent_history_version: 0,
|
||||
@@ -439,14 +449,15 @@ async fn build_guardian_prompt_stale_delta_cursor_falls_back_to_full_prompt() ->
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-3".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Delta {
|
||||
cursor: GuardianTranscriptCursor {
|
||||
parent_history_version: 0,
|
||||
@@ -520,14 +531,15 @@ async fn build_guardian_prompt_stale_delta_version_falls_back_to_full_prompt() -
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-4".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push after the compaction.".to_string()),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Delta {
|
||||
cursor: GuardianTranscriptCursor {
|
||||
parent_history_version: 0,
|
||||
@@ -684,12 +696,13 @@ fn guardian_truncate_text_keeps_prefix_suffix_and_xml_marker() {
|
||||
#[test]
|
||||
fn format_guardian_action_pretty_truncates_large_string_fields() -> serde_json::Result<()> {
|
||||
let patch = "line\n".repeat(100_000);
|
||||
let action = GuardianApprovalRequest::ApplyPatch {
|
||||
let action = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
files: Vec::new(),
|
||||
changes: HashMap::new(),
|
||||
patch: patch.clone(),
|
||||
};
|
||||
});
|
||||
|
||||
let rendered = format_guardian_action_pretty(&action)?;
|
||||
|
||||
@@ -703,12 +716,13 @@ fn format_guardian_action_pretty_truncates_large_string_fields() -> serde_json::
|
||||
#[test]
|
||||
fn format_guardian_action_pretty_reports_no_truncation_for_small_payload() -> serde_json::Result<()>
|
||||
{
|
||||
let action = GuardianApprovalRequest::ApplyPatch {
|
||||
let action = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
files: Vec::new(),
|
||||
changes: HashMap::new(),
|
||||
patch: "line\n".to_string(),
|
||||
};
|
||||
});
|
||||
|
||||
let rendered = format_guardian_action_pretty(&action)?;
|
||||
|
||||
@@ -719,10 +733,11 @@ fn format_guardian_action_pretty_reports_no_truncation_for_small_payload() -> se
|
||||
|
||||
#[test]
|
||||
fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json::Result<()> {
|
||||
let action = GuardianApprovalRequest::McpToolCall {
|
||||
let action = ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
server: "mcp_server".to_string(),
|
||||
tool_name: "browser_navigate".to_string(),
|
||||
hook_tool_name: "browser_navigate".to_string(),
|
||||
arguments: Some(serde_json::json!({
|
||||
"url": "https://example.com",
|
||||
})),
|
||||
@@ -736,7 +751,7 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json
|
||||
open_world_hint: None,
|
||||
read_only_hint: Some(false),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
guardian_approval_request_to_json(&action)?,
|
||||
@@ -761,10 +776,12 @@ fn guardian_approval_request_to_json_renders_mcp_tool_call_shape() -> serde_json
|
||||
#[test]
|
||||
fn guardian_approval_request_to_json_renders_network_access_trigger() -> serde_json::Result<()> {
|
||||
let cwd = test_path_buf("/repo").abs();
|
||||
let action = GuardianApprovalRequest::NetworkAccess {
|
||||
let action = ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
target: "https://example.com:443".to_string(),
|
||||
hook_command: "curl https://example.com".to_string(),
|
||||
cwd: cwd.clone(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
@@ -778,7 +795,7 @@ fn guardian_approval_request_to_json_renders_network_access_trigger() -> serde_j
|
||||
justification: Some("Fetch the release metadata.".to_string()),
|
||||
tty: None,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
guardian_approval_request_to_json(&action)?,
|
||||
@@ -811,10 +828,12 @@ async fn build_guardian_prompt_items_explains_network_access_review_scope() -> a
|
||||
let prompt = build_guardian_prompt_items(
|
||||
session.as_ref(),
|
||||
Some("Network access to \"example.com\" is blocked by policy.".to_string()),
|
||||
GuardianApprovalRequest::NetworkAccess {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
target: "https://example.com:443".to_string(),
|
||||
hook_command: "curl https://example.com".to_string(),
|
||||
cwd: cwd.clone(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
@@ -828,7 +847,7 @@ async fn build_guardian_prompt_items_explains_network_access_review_scope() -> a
|
||||
justification: Some("Fetch the release metadata.".to_string()),
|
||||
tty: None,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Full,
|
||||
)
|
||||
.await?;
|
||||
@@ -875,13 +894,14 @@ async fn build_guardian_prompt_items_explains_network_access_review_scope() -> a
|
||||
fn guardian_assessment_action_redacts_apply_patch_patch_text() {
|
||||
let cwd = test_path_buf("/tmp").abs();
|
||||
let file = test_path_buf("/tmp/guardian.txt").abs();
|
||||
let action = GuardianApprovalRequest::ApplyPatch {
|
||||
let action = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: cwd.clone(),
|
||||
files: vec![file.clone()],
|
||||
changes: HashMap::new(),
|
||||
patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+secret\n*** End Patch"
|
||||
.to_string(),
|
||||
};
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(guardian_assessment_action(&action)).expect("serialize action"),
|
||||
@@ -895,22 +915,25 @@ fn guardian_assessment_action_redacts_apply_patch_patch_text() {
|
||||
|
||||
#[test]
|
||||
fn guardian_request_turn_id_prefers_network_access_owner_turn() {
|
||||
let network_access = GuardianApprovalRequest::NetworkAccess {
|
||||
let network_access = ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "owner-turn".to_string(),
|
||||
target: "https://example.com:443".to_string(),
|
||||
hook_command: "network-access https://example.com:443".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
trigger: None,
|
||||
};
|
||||
let apply_patch = GuardianApprovalRequest::ApplyPatch {
|
||||
});
|
||||
let apply_patch = ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
files: vec![test_path_buf("/tmp/guardian.txt").abs()],
|
||||
changes: HashMap::new(),
|
||||
patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch"
|
||||
.to_string(),
|
||||
};
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
guardian_request_turn_id(&network_access, "fallback-turn"),
|
||||
@@ -924,10 +947,12 @@ fn guardian_request_turn_id_prefers_network_access_owner_turn() {
|
||||
|
||||
#[test]
|
||||
fn guardian_request_target_item_id_omits_network_access_trigger_call_id() {
|
||||
let network_access = GuardianApprovalRequest::NetworkAccess {
|
||||
let network_access = ApprovalRequest::Command(CommandApprovalRequest::NetworkAccess {
|
||||
id: "network-1".to_string(),
|
||||
turn_id: "owner-turn".to_string(),
|
||||
target: "https://example.com:443".to_string(),
|
||||
hook_command: "network-access https://example.com:443".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
host: "example.com".to_string(),
|
||||
protocol: NetworkApprovalProtocol::Https,
|
||||
port: 443,
|
||||
@@ -941,7 +966,7 @@ fn guardian_request_target_item_id_omits_network_access_trigger_call_id() {
|
||||
justification: None,
|
||||
tty: None,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
assert_eq!(guardian_request_target_item_id(&network_access), None);
|
||||
}
|
||||
@@ -956,13 +981,14 @@ async fn cancelled_guardian_review_emits_terminal_abort_without_warning() {
|
||||
&session,
|
||||
&turn,
|
||||
"review-cancelled-guardian".to_string(),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
ApprovalRequest::ApplyPatch(ApplyPatchApprovalRequest {
|
||||
id: "patch-1".to_string(),
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
files: vec![test_path_buf("/tmp/guardian.txt").abs()],
|
||||
changes: HashMap::new(),
|
||||
patch: "*** Begin Patch\n*** Update File: guardian.txt\n@@\n+hello\n*** End Patch"
|
||||
.to_string(),
|
||||
},
|
||||
}),
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequestSource::MainTurn,
|
||||
cancel_token,
|
||||
@@ -1245,7 +1271,7 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
|
||||
let turn = Arc::new(turn);
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
|
||||
let request = GuardianApprovalRequest::Shell {
|
||||
let request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec![
|
||||
"git".to_string(),
|
||||
@@ -1253,11 +1279,12 @@ async fn guardian_review_request_layout_matches_model_visible_request_snapshot()
|
||||
"origin".to_string(),
|
||||
"guardian-approval-mvp".to_string(),
|
||||
],
|
||||
hook_command: "git push origin guardian-approval-mvp".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the reviewed docs fix to the repo remote.".to_string()),
|
||||
};
|
||||
});
|
||||
|
||||
let outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
@@ -1355,14 +1382,15 @@ async fn build_guardian_prompt_items_includes_parent_session_id() -> anyhow::Res
|
||||
let prompt = build_guardian_prompt_items(
|
||||
&session,
|
||||
/*retry_reason*/ None,
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
hook_command: "git status".to_string(),
|
||||
cwd: test_path_buf("/repo").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
},
|
||||
}),
|
||||
GuardianPromptMode::Full,
|
||||
)
|
||||
.await?;
|
||||
@@ -1429,14 +1457,15 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
let (session, turn) = guardian_test_session_and_turn(&server).await;
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
|
||||
let first_request = GuardianApprovalRequest::Shell {
|
||||
let first_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the first docs fix.".to_string()),
|
||||
};
|
||||
});
|
||||
let first_outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
@@ -1469,18 +1498,19 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
turn.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let second_request = GuardianApprovalRequest::Shell {
|
||||
let second_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-2".to_string(),
|
||||
command: vec![
|
||||
"git".to_string(),
|
||||
"push".to_string(),
|
||||
"--force-with-lease".to_string(),
|
||||
],
|
||||
hook_command: "git push --force-with-lease".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the second docs fix.".to_string()),
|
||||
};
|
||||
});
|
||||
let second_outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
@@ -1513,14 +1543,15 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
turn.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let third_request = GuardianApprovalRequest::Shell {
|
||||
let third_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-3".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the third docs fix.".to_string()),
|
||||
};
|
||||
});
|
||||
let third_outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
@@ -1708,14 +1739,15 @@ async fn guardian_reused_trunk_ignores_stale_prior_turn_completion() -> anyhow::
|
||||
let first_outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-1".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the first docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
/*retry_reason*/ None,
|
||||
guardian_output_schema(),
|
||||
/*external_cancel*/ None,
|
||||
@@ -1750,14 +1782,15 @@ async fn guardian_reused_trunk_ignores_stale_prior_turn_completion() -> anyhow::
|
||||
let second_outcome = run_guardian_review_session_for_test(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-2".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the second docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
/*retry_reason*/ None,
|
||||
guardian_output_schema(),
|
||||
/*external_cancel*/ None,
|
||||
@@ -1829,14 +1862,15 @@ async fn guardian_review_surfaces_responses_api_errors_in_rejection_reason() ->
|
||||
&session,
|
||||
&turn,
|
||||
"review-shell-guardian-error".to_string(),
|
||||
GuardianApprovalRequest::Shell {
|
||||
ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-guardian-error".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Need to push the reviewed docs fix.".to_string()),
|
||||
},
|
||||
}),
|
||||
/*retry_reason*/ None,
|
||||
)
|
||||
.await;
|
||||
@@ -1963,14 +1997,15 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
let (session, turn) = guardian_test_session_and_turn_with_base_url(server.uri()).await;
|
||||
seed_guardian_parent_history(&session, &turn).await;
|
||||
|
||||
let initial_request = GuardianApprovalRequest::Shell {
|
||||
let initial_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-guardian-1".to_string(),
|
||||
command: vec!["git".to_string(), "status".to_string()],
|
||||
hook_command: "git status".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Inspect repo state before proceeding.".to_string()),
|
||||
};
|
||||
});
|
||||
assert_eq!(
|
||||
review_approval_request(
|
||||
&session,
|
||||
@@ -2006,22 +2041,24 @@ async fn guardian_parallel_reviews_fork_from_last_committed_trunk_history() -> a
|
||||
)
|
||||
.await;
|
||||
|
||||
let second_request = GuardianApprovalRequest::Shell {
|
||||
let second_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-guardian-2".to_string(),
|
||||
command: vec!["git".to_string(), "diff".to_string()],
|
||||
hook_command: "git diff".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Inspect pending changes before proceeding.".to_string()),
|
||||
};
|
||||
let third_request = GuardianApprovalRequest::Shell {
|
||||
});
|
||||
let third_request = ApprovalRequest::Command(CommandApprovalRequest::Shell {
|
||||
id: "shell-guardian-3".to_string(),
|
||||
command: vec!["git".to_string(), "push".to_string()],
|
||||
hook_command: "git push".to_string(),
|
||||
cwd: test_path_buf("/repo/codex-rs/core").abs(),
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: Some("Inspect whether pushing is safe before proceeding.".to_string()),
|
||||
};
|
||||
});
|
||||
|
||||
let session_for_second = Arc::clone(&session);
|
||||
let turn_for_second = Arc::clone(&turn);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod apply_patch;
|
||||
mod approval_request;
|
||||
mod apps;
|
||||
mod arc_monitor;
|
||||
mod client;
|
||||
|
||||
@@ -10,6 +10,9 @@ use codex_app_server_protocol::McpServerElicitationRequest;
|
||||
use codex_app_server_protocol::McpServerElicitationRequestParams;
|
||||
use tracing::error;
|
||||
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::GuardianMcpAnnotations;
|
||||
use crate::approval_request::McpToolCallApprovalRequest;
|
||||
use crate::arc_monitor::ArcMonitorOutcome;
|
||||
use crate::arc_monitor::monitor_action;
|
||||
use crate::config::Config;
|
||||
@@ -17,8 +20,6 @@ use crate::config::edit::ConfigEdit;
|
||||
use crate::config::edit::ConfigEditsBuilder;
|
||||
use crate::config::load_global_mcp_servers;
|
||||
use crate::connectors;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianMcpAnnotations;
|
||||
use crate::guardian::guardian_approval_request_to_json;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
@@ -31,8 +32,6 @@ use crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam;
|
||||
use crate::mcp_tool_approval_templates::render_mcp_tool_approval_template;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use codex_analytics::AppInvocation;
|
||||
use codex_analytics::InvocationType;
|
||||
use codex_analytics::build_track_events_context;
|
||||
@@ -885,7 +884,7 @@ struct McpToolApprovalPromptOptions {
|
||||
|
||||
struct McpToolApprovalElicitationRequest<'a> {
|
||||
server: &'a str,
|
||||
metadata: Option<&'a McpToolApprovalMetadata>,
|
||||
approval_request: &'a ApprovalRequest,
|
||||
tool_params: Option<&'a serde_json::Value>,
|
||||
tool_params_display: Option<&'a [RenderedMcpToolApprovalParam]>,
|
||||
question: RequestUserInputQuestion,
|
||||
@@ -893,6 +892,14 @@ struct McpToolApprovalElicitationRequest<'a> {
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct McpToolApprovalPrompt {
|
||||
question: RequestUserInputQuestion,
|
||||
message_override: Option<String>,
|
||||
tool_params: Option<JsonValue>,
|
||||
tool_params_display: Option<Vec<RenderedMcpToolApprovalParam>>,
|
||||
}
|
||||
|
||||
pub(crate) const MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX: &str = "mcp_tool_call_approval";
|
||||
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT: &str = "Allow";
|
||||
pub(crate) const MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION: &str = "Allow for this session";
|
||||
@@ -917,6 +924,7 @@ const MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: &str = "tool_title";
|
||||
const MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: &str = "tool_description";
|
||||
const MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: &str = "tool_params";
|
||||
const MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: &str = "tool_params_display";
|
||||
|
||||
const MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_DEFAULT: &str = "mcp_tool_call__default";
|
||||
const MCP_TOOL_CALL_ARC_MONITOR_CALLSITE_ALWAYS_ALLOW: &str = "mcp_tool_call__always_allow";
|
||||
|
||||
@@ -933,6 +941,105 @@ struct McpToolApprovalKey {
|
||||
tool_name: String,
|
||||
}
|
||||
|
||||
/// Builds the user-facing MCP approval prompt from the canonical approval request.
|
||||
fn mcp_tool_approval_prompt(
|
||||
approval_request: &ApprovalRequest,
|
||||
question_id: String,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
monitor_reason: Option<&str>,
|
||||
) -> Option<McpToolApprovalPrompt> {
|
||||
let ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
server,
|
||||
tool_name,
|
||||
arguments,
|
||||
connector_id,
|
||||
connector_name,
|
||||
tool_title,
|
||||
..
|
||||
}) = approval_request
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let rendered_template = render_mcp_tool_approval_template(
|
||||
server,
|
||||
connector_id.as_deref(),
|
||||
connector_name.as_deref(),
|
||||
tool_title.as_deref(),
|
||||
arguments.as_ref(),
|
||||
);
|
||||
let tool_params_display = rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.tool_params_display.clone())
|
||||
.or_else(|| build_mcp_tool_approval_display_params(arguments.as_ref()));
|
||||
let question_override = rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.question.as_str());
|
||||
let mut question = build_mcp_tool_approval_question(
|
||||
question_id,
|
||||
server,
|
||||
tool_name,
|
||||
connector_name.as_deref(),
|
||||
prompt_options,
|
||||
question_override,
|
||||
);
|
||||
question.question = mcp_tool_approval_question_text(question.question, monitor_reason);
|
||||
|
||||
Some(McpToolApprovalPrompt {
|
||||
question,
|
||||
message_override: rendered_template.as_ref().and_then(|rendered_template| {
|
||||
monitor_reason
|
||||
.is_none()
|
||||
.then_some(rendered_template.elicitation_message.clone())
|
||||
}),
|
||||
tool_params: rendered_template
|
||||
.as_ref()
|
||||
.and_then(|rendered_template| rendered_template.tool_params.clone())
|
||||
.or_else(|| arguments.clone()),
|
||||
tool_params_display,
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts a guardian MCP approval decision into the legacy RequestUserInput
|
||||
/// response shape used by delegated compatibility flows.
|
||||
pub(crate) fn mcp_tool_approval_compat_response(
|
||||
approval_request: &ApprovalRequest,
|
||||
question: &RequestUserInputQuestion,
|
||||
decision: ReviewDecision,
|
||||
) -> Option<RequestUserInputResponse> {
|
||||
let ApprovalRequest::McpToolCall(..) = approval_request else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let selected_label = match decision {
|
||||
ReviewDecision::ApprovedForSession => question
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| {
|
||||
options
|
||||
.iter()
|
||||
.find(|option| option.label == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)
|
||||
})
|
||||
.map(|option| option.label.clone())
|
||||
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
|
||||
ReviewDecision::Denied | ReviewDecision::TimedOut | ReviewDecision::Abort => {
|
||||
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
Some(RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
question.id.clone(),
|
||||
RequestUserInputAnswer {
|
||||
answers: vec![selected_label],
|
||||
},
|
||||
)]),
|
||||
})
|
||||
}
|
||||
|
||||
fn mcp_tool_approval_prompt_options(
|
||||
session_approval_key: Option<&McpToolApprovalKey>,
|
||||
persistent_approval_key: Option<&McpToolApprovalKey>,
|
||||
@@ -1005,18 +1112,15 @@ async fn maybe_request_mcp_tool_approval(
|
||||
return Some(McpToolApprovalDecision::Accept);
|
||||
}
|
||||
|
||||
match run_permission_request_hooks(
|
||||
sess,
|
||||
turn_context,
|
||||
call_id,
|
||||
PermissionRequestPayload {
|
||||
tool_name: HookToolName::new(hook_tool_name),
|
||||
tool_input: invocation
|
||||
.arguments
|
||||
.clone()
|
||||
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new())),
|
||||
},
|
||||
)
|
||||
let approval_request =
|
||||
build_mcp_tool_approval_request(call_id, hook_tool_name, invocation, metadata);
|
||||
|
||||
match run_permission_request_hooks(sess, turn_context, call_id, {
|
||||
let Some(payload) = approval_request.permission_request_payload() else {
|
||||
unreachable!("MCP approvals always project a permission request payload");
|
||||
};
|
||||
payload
|
||||
})
|
||||
.await
|
||||
{
|
||||
Some(PermissionRequestDecision::Allow) => {
|
||||
@@ -1041,7 +1145,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
sess,
|
||||
turn_context,
|
||||
review_id.clone(),
|
||||
build_guardian_mcp_tool_review_request(call_id, invocation, metadata),
|
||||
approval_request,
|
||||
monitor_reason.clone(),
|
||||
)
|
||||
.await;
|
||||
@@ -1063,50 +1167,26 @@ async fn maybe_request_mcp_tool_approval(
|
||||
tool_call_mcp_elicitation_enabled,
|
||||
);
|
||||
let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}");
|
||||
let rendered_template = render_mcp_tool_approval_template(
|
||||
&invocation.server,
|
||||
metadata.and_then(|metadata| metadata.connector_id.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
metadata.and_then(|metadata| metadata.tool_title.as_deref()),
|
||||
invocation.arguments.as_ref(),
|
||||
);
|
||||
let tool_params_display = rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.tool_params_display.clone())
|
||||
.or_else(|| build_mcp_tool_approval_display_params(invocation.arguments.as_ref()));
|
||||
let mut question = build_mcp_tool_approval_question(
|
||||
let Some(prompt) = mcp_tool_approval_prompt(
|
||||
&approval_request,
|
||||
question_id.clone(),
|
||||
&invocation.server,
|
||||
&invocation.tool,
|
||||
metadata.and_then(|metadata| metadata.connector_name.as_deref()),
|
||||
prompt_options,
|
||||
rendered_template
|
||||
.as_ref()
|
||||
.map(|rendered_template| rendered_template.question.as_str()),
|
||||
);
|
||||
question.question =
|
||||
mcp_tool_approval_question_text(question.question, monitor_reason.as_deref());
|
||||
monitor_reason.as_deref(),
|
||||
) else {
|
||||
unreachable!("MCP approvals always project an MCP approval prompt");
|
||||
};
|
||||
if tool_call_mcp_elicitation_enabled {
|
||||
let request_id = rmcp::model::RequestId::String(
|
||||
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}").into(),
|
||||
);
|
||||
let request_id = rmcp::model::RequestId::String(question_id.clone().into());
|
||||
let params = build_mcp_tool_approval_elicitation_request(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: &invocation.server,
|
||||
metadata,
|
||||
tool_params: rendered_template
|
||||
.as_ref()
|
||||
.and_then(|rendered_template| rendered_template.tool_params.as_ref())
|
||||
.or(invocation.arguments.as_ref()),
|
||||
tool_params_display: tool_params_display.as_deref(),
|
||||
question,
|
||||
message_override: rendered_template.as_ref().and_then(|rendered_template| {
|
||||
monitor_reason
|
||||
.is_none()
|
||||
.then_some(rendered_template.elicitation_message.as_str())
|
||||
}),
|
||||
approval_request: &approval_request,
|
||||
tool_params: prompt.tool_params.as_ref(),
|
||||
tool_params_display: prompt.tool_params_display.as_deref(),
|
||||
question: prompt.question,
|
||||
message_override: prompt.message_override.as_deref(),
|
||||
prompt_options,
|
||||
},
|
||||
);
|
||||
@@ -1128,7 +1208,7 @@ async fn maybe_request_mcp_tool_approval(
|
||||
}
|
||||
|
||||
let args = RequestUserInputArgs {
|
||||
questions: vec![question],
|
||||
questions: vec![prompt.question],
|
||||
};
|
||||
let response = sess
|
||||
.request_user_input(turn_context.as_ref(), call_id.to_string(), args)
|
||||
@@ -1169,7 +1249,8 @@ fn prepare_arc_request_action(
|
||||
invocation: &McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
) -> serde_json::Value {
|
||||
let request = build_guardian_mcp_tool_review_request("arc-monitor", invocation, metadata);
|
||||
let request =
|
||||
build_mcp_tool_approval_request("arc-monitor", &invocation.tool, invocation, metadata);
|
||||
match guardian_approval_request_to_json(&request) {
|
||||
Ok(action) => action,
|
||||
Err(error) => {
|
||||
@@ -1208,15 +1289,17 @@ fn persistent_mcp_tool_approval_key(
|
||||
session_mcp_tool_approval_key(invocation, metadata, approval_mode)
|
||||
}
|
||||
|
||||
pub(crate) fn build_guardian_mcp_tool_review_request(
|
||||
pub(crate) fn build_mcp_tool_approval_request(
|
||||
call_id: &str,
|
||||
hook_tool_name: &str,
|
||||
invocation: &McpInvocation,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
) -> GuardianApprovalRequest {
|
||||
GuardianApprovalRequest::McpToolCall {
|
||||
) -> ApprovalRequest {
|
||||
McpToolCallApprovalRequest {
|
||||
id: call_id.to_string(),
|
||||
server: invocation.server.clone(),
|
||||
tool_name: invocation.tool.clone(),
|
||||
hook_tool_name: hook_tool_name.to_string(),
|
||||
arguments: invocation.arguments.clone(),
|
||||
connector_id: metadata.and_then(|metadata| metadata.connector_id.clone()),
|
||||
connector_name: metadata.and_then(|metadata| metadata.connector_name.clone()),
|
||||
@@ -1231,6 +1314,7 @@ pub(crate) fn build_guardian_mcp_tool_review_request(
|
||||
read_only_hint: annotations.read_only_hint,
|
||||
}),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
async fn mcp_tool_approval_decision_from_guardian(
|
||||
@@ -1489,8 +1573,7 @@ fn build_mcp_tool_approval_elicitation_request(
|
||||
server_name: request.server.to_string(),
|
||||
request: McpServerElicitationRequest::Form {
|
||||
meta: build_mcp_tool_approval_elicitation_meta(
|
||||
request.server,
|
||||
request.metadata,
|
||||
request.approval_request,
|
||||
request.tool_params,
|
||||
request.tool_params_display,
|
||||
request.prompt_options,
|
||||
@@ -1507,16 +1590,28 @@ fn build_mcp_tool_approval_elicitation_request(
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_elicitation_meta(
|
||||
server: &str,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
approval_request: &ApprovalRequest,
|
||||
tool_params: Option<&JsonValue>,
|
||||
tool_params_display: Option<&[RenderedMcpToolApprovalParam]>,
|
||||
prompt_options: McpToolApprovalPromptOptions,
|
||||
) -> Option<serde_json::Value> {
|
||||
) -> Option<JsonValue> {
|
||||
let ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
server,
|
||||
connector_id,
|
||||
connector_name,
|
||||
connector_description,
|
||||
tool_title,
|
||||
tool_description,
|
||||
..
|
||||
}) = approval_request
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut meta = serde_json::Map::new();
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_KIND_KEY.to_string(),
|
||||
serde_json::Value::String(MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL.to_string()),
|
||||
JsonValue::String(MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL.to_string()),
|
||||
);
|
||||
match (
|
||||
prompt_options.allow_session_remember,
|
||||
@@ -1534,57 +1629,53 @@ fn build_mcp_tool_approval_elicitation_meta(
|
||||
(true, false) => {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),
|
||||
serde_json::Value::String(MCP_TOOL_APPROVAL_PERSIST_SESSION.to_string()),
|
||||
JsonValue::String(MCP_TOOL_APPROVAL_PERSIST_SESSION.to_string()),
|
||||
);
|
||||
}
|
||||
(false, true) => {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_PERSIST_KEY.to_string(),
|
||||
serde_json::Value::String(MCP_TOOL_APPROVAL_PERSIST_ALWAYS.to_string()),
|
||||
JsonValue::String(MCP_TOOL_APPROVAL_PERSIST_ALWAYS.to_string()),
|
||||
);
|
||||
}
|
||||
(false, false) => {}
|
||||
}
|
||||
if let Some(metadata) = metadata {
|
||||
if let Some(tool_title) = metadata.tool_title.as_ref() {
|
||||
if let Some(tool_title) = tool_title.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY.to_string(),
|
||||
JsonValue::String(tool_title.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(tool_description) = tool_description.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY.to_string(),
|
||||
JsonValue::String(tool_description.clone()),
|
||||
);
|
||||
}
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& (connector_id.is_some() || connector_name.is_some() || connector_description.is_some())
|
||||
{
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_SOURCE_KEY.to_string(),
|
||||
JsonValue::String(MCP_TOOL_APPROVAL_SOURCE_CONNECTOR.to_string()),
|
||||
);
|
||||
if let Some(connector_id) = connector_id.as_deref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY.to_string(),
|
||||
serde_json::Value::String(tool_title.clone()),
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY.to_string(),
|
||||
JsonValue::String(connector_id.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(tool_description) = metadata.tool_description.as_ref() {
|
||||
if let Some(connector_name) = connector_name.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY.to_string(),
|
||||
serde_json::Value::String(tool_description.clone()),
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY.to_string(),
|
||||
JsonValue::String(connector_name.clone()),
|
||||
);
|
||||
}
|
||||
if server == CODEX_APPS_MCP_SERVER_NAME
|
||||
&& (metadata.connector_id.is_some()
|
||||
|| metadata.connector_name.is_some()
|
||||
|| metadata.connector_description.is_some())
|
||||
{
|
||||
if let Some(connector_description) = connector_description.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_SOURCE_KEY.to_string(),
|
||||
serde_json::Value::String(MCP_TOOL_APPROVAL_SOURCE_CONNECTOR.to_string()),
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY.to_string(),
|
||||
JsonValue::String(connector_description.clone()),
|
||||
);
|
||||
if let Some(connector_id) = metadata.connector_id.as_deref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY.to_string(),
|
||||
serde_json::Value::String(connector_id.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(connector_name) = metadata.connector_name.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY.to_string(),
|
||||
serde_json::Value::String(connector_name.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(connector_description) = metadata.connector_description.as_ref() {
|
||||
meta.insert(
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY.to_string(),
|
||||
serde_json::Value::String(connector_description.clone()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(tool_params) = tool_params {
|
||||
@@ -1601,22 +1692,21 @@ fn build_mcp_tool_approval_elicitation_meta(
|
||||
tool_params_display,
|
||||
);
|
||||
}
|
||||
(!meta.is_empty()).then_some(serde_json::Value::Object(meta))
|
||||
|
||||
(!meta.is_empty()).then_some(JsonValue::Object(meta))
|
||||
}
|
||||
|
||||
fn build_mcp_tool_approval_display_params(
|
||||
tool_params: Option<&serde_json::Value>,
|
||||
) -> Option<Vec<crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam>> {
|
||||
tool_params: Option<&JsonValue>,
|
||||
) -> Option<Vec<RenderedMcpToolApprovalParam>> {
|
||||
let tool_params = tool_params?.as_object()?;
|
||||
let mut display_params = tool_params
|
||||
.iter()
|
||||
.map(
|
||||
|(name, value)| crate::mcp_tool_approval_templates::RenderedMcpToolApprovalParam {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
display_name: name.clone(),
|
||||
},
|
||||
)
|
||||
.map(|(name, value)| RenderedMcpToolApprovalParam {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
display_name: name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
display_params.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
Some(display_params)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::*;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::GuardianMcpAnnotations;
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::session::tests::make_session_and_context_with_rx;
|
||||
@@ -103,6 +105,24 @@ fn prompt_options(
|
||||
}
|
||||
}
|
||||
|
||||
fn approval_prompt_request(
|
||||
server: &str,
|
||||
tool_name: &str,
|
||||
arguments: Option<serde_json::Value>,
|
||||
metadata: Option<&McpToolApprovalMetadata>,
|
||||
) -> ApprovalRequest {
|
||||
build_mcp_tool_approval_request(
|
||||
"call-1",
|
||||
tool_name,
|
||||
&McpInvocation {
|
||||
server: server.to_string(),
|
||||
tool: tool_name.to_string(),
|
||||
arguments,
|
||||
},
|
||||
metadata,
|
||||
)
|
||||
}
|
||||
|
||||
fn install_mcp_permission_request_hook(
|
||||
session: &mut Session,
|
||||
turn_context: &TurnContext,
|
||||
@@ -280,15 +300,97 @@ fn prompt_mode_does_not_allow_persistent_remember() {
|
||||
|
||||
#[test]
|
||||
fn approval_question_text_prepends_safety_reason() {
|
||||
let request = approval_prompt_request(
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
mcp_tool_approval_question_text(
|
||||
"Allow this action?".to_string(),
|
||||
mcp_tool_approval_prompt(
|
||||
&request,
|
||||
"q".to_string(),
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ false, /*allow_persistent_approval*/ false,
|
||||
),
|
||||
Some("This tool may contact an external system."),
|
||||
),
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question
|
||||
.question,
|
||||
"Tool call needs your approval. Reason: This tool may contact an external system."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_approval_compat_response_uses_session_label_when_present() {
|
||||
let request = approval_prompt_request(
|
||||
"custom_server",
|
||||
"dangerous_tool",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
);
|
||||
let question = RequestUserInputQuestion {
|
||||
id: "q-1".to_string(),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: Some(vec![RequestUserInputQuestionOption {
|
||||
label: MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string(),
|
||||
description: "Remember until session ends".to_string(),
|
||||
}]),
|
||||
};
|
||||
|
||||
let response =
|
||||
mcp_tool_approval_compat_response(&request, &question, ReviewDecision::ApprovedForSession)
|
||||
.expect("compat response");
|
||||
|
||||
assert_eq!(
|
||||
response.answers.get("q-1"),
|
||||
Some(&RequestUserInputAnswer {
|
||||
answers: vec![MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION.to_string()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_approval_compat_response_uses_synthetic_decline_for_abort() {
|
||||
let request = approval_prompt_request(
|
||||
"custom_server",
|
||||
"dangerous_tool",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
);
|
||||
let question = RequestUserInputQuestion {
|
||||
id: "q-1".to_string(),
|
||||
header: "Approve app tool call?".to_string(),
|
||||
question: "Allow this app tool?".to_string(),
|
||||
is_other: false,
|
||||
is_secret: false,
|
||||
options: None,
|
||||
};
|
||||
|
||||
let response = mcp_tool_approval_compat_response(&request, &question, ReviewDecision::Abort)
|
||||
.expect("compat response");
|
||||
|
||||
assert_eq!(
|
||||
response.answers.get("q-1"),
|
||||
Some(&RequestUserInputAnswer {
|
||||
answers: vec![MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_tool_approval_question_id_detection_round_trips() {
|
||||
let question_id = format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_call-1");
|
||||
|
||||
assert_eq!(question_id, "mcp_tool_call_approval_call-1");
|
||||
assert!(is_mcp_tool_approval_question_id(&question_id));
|
||||
assert!(!is_mcp_tool_approval_question_id("other_question"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tool_call_span_records_expected_fields() {
|
||||
let buffer: &'static std::sync::Mutex<Vec<u8>> =
|
||||
@@ -479,47 +581,43 @@ fn truncates_strings_on_char_boundaries() {
|
||||
#[tokio::test]
|
||||
async fn approval_elicitation_request_uses_message_override_and_preserves_tool_params_keys() {
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
let metadata = approval_metadata(
|
||||
Some("connector_947e0d954944416db111db556030eea6"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("create_event"),
|
||||
Some("Create a calendar event."),
|
||||
);
|
||||
let approval_request = approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"create_event",
|
||||
Some("Calendar"),
|
||||
Some(serde_json::json!({
|
||||
"title": "Roadmap review",
|
||||
"start_time": "2026-05-01T10:00:00Z",
|
||||
"attendees": ["ada@example.com"],
|
||||
})),
|
||||
Some(&metadata),
|
||||
);
|
||||
let prompt = mcp_tool_approval_prompt(
|
||||
&approval_request,
|
||||
"q".to_string(),
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
Some("Allow Calendar to create an event?"),
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt");
|
||||
|
||||
let request = build_mcp_tool_approval_elicitation_request(
|
||||
&session,
|
||||
&turn_context,
|
||||
McpToolApprovalElicitationRequest {
|
||||
server: CODEX_APPS_MCP_SERVER_NAME,
|
||||
metadata: Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Create Event"),
|
||||
Some("Create a calendar event."),
|
||||
)),
|
||||
tool_params: Some(&serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
"title": "Roadmap review",
|
||||
})),
|
||||
tool_params_display: Some(&[
|
||||
RenderedMcpToolApprovalParam {
|
||||
name: "calendar_id".to_string(),
|
||||
value: serde_json::json!("primary"),
|
||||
display_name: "Calendar".to_string(),
|
||||
},
|
||||
RenderedMcpToolApprovalParam {
|
||||
name: "title".to_string(),
|
||||
value: serde_json::json!("Roadmap review"),
|
||||
display_name: "Title".to_string(),
|
||||
},
|
||||
]),
|
||||
question,
|
||||
message_override: Some("Allow Calendar to create an event?"),
|
||||
approval_request: &approval_request,
|
||||
tool_params: prompt.tool_params.as_ref(),
|
||||
tool_params_display: prompt.tool_params_display.as_deref(),
|
||||
question: prompt.question,
|
||||
message_override: prompt.message_override.as_deref(),
|
||||
prompt_options: prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
@@ -540,26 +638,32 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
|
||||
MCP_TOOL_APPROVAL_PERSIST_ALWAYS,
|
||||
],
|
||||
MCP_TOOL_APPROVAL_SOURCE_KEY: MCP_TOOL_APPROVAL_SOURCE_CONNECTOR,
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "calendar",
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "connector_947e0d954944416db111db556030eea6",
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: "Calendar",
|
||||
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: "Manage events and schedules.",
|
||||
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "Create Event",
|
||||
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "create_event",
|
||||
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: "Create a calendar event.",
|
||||
MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: {
|
||||
"calendar_id": "primary",
|
||||
"title": "Roadmap review",
|
||||
"start_time": "2026-05-01T10:00:00Z",
|
||||
"attendees": ["ada@example.com"],
|
||||
},
|
||||
MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: [
|
||||
{
|
||||
"name": "calendar_id",
|
||||
"value": "primary",
|
||||
"display_name": "Calendar",
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"value": "Roadmap review",
|
||||
"display_name": "Title",
|
||||
},
|
||||
{
|
||||
"name": "start_time",
|
||||
"value": "2026-05-01T10:00:00Z",
|
||||
"display_name": "Start",
|
||||
},
|
||||
{
|
||||
"name": "attendees",
|
||||
"value": ["ada@example.com"],
|
||||
"display_name": "Attendees",
|
||||
},
|
||||
],
|
||||
})),
|
||||
message: "Allow Calendar to create an event?".to_string(),
|
||||
@@ -576,16 +680,21 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
|
||||
|
||||
#[test]
|
||||
fn custom_mcp_tool_question_mentions_server_name() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
let question = mcp_tool_approval_prompt(
|
||||
&approval_prompt_request(
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
),
|
||||
"q".to_string(),
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ false, /*allow_persistent_approval*/ false,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question;
|
||||
|
||||
assert_eq!(question.header, "Approve app tool call?");
|
||||
assert_eq!(
|
||||
@@ -604,16 +713,21 @@ fn custom_mcp_tool_question_mentions_server_name() {
|
||||
|
||||
#[test]
|
||||
fn codex_apps_tool_question_uses_fallback_app_label() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
let question = mcp_tool_approval_prompt(
|
||||
&approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
),
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question;
|
||||
|
||||
assert_eq!(
|
||||
question.question,
|
||||
@@ -623,16 +737,28 @@ fn codex_apps_tool_question_uses_fallback_app_label() {
|
||||
|
||||
#[test]
|
||||
fn trusted_codex_apps_tool_question_offers_always_allow() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
let metadata = approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
/*connector_description*/ None,
|
||||
/*tool_title*/ None,
|
||||
/*tool_description*/ None,
|
||||
);
|
||||
let question = mcp_tool_approval_prompt(
|
||||
&approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
Some(&metadata),
|
||||
),
|
||||
"q".to_string(),
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question;
|
||||
let options = question.options.expect("options");
|
||||
|
||||
assert!(options.iter().any(|option| {
|
||||
@@ -665,18 +791,30 @@ fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
|
||||
tool_name: "run_action".to_string(),
|
||||
};
|
||||
let persistent_key = session_key.clone();
|
||||
let question = build_mcp_tool_approval_question(
|
||||
"q".to_string(),
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
let metadata = approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
/*connector_description*/ None,
|
||||
/*tool_title*/ None,
|
||||
/*tool_description*/ None,
|
||||
);
|
||||
let question = mcp_tool_approval_prompt(
|
||||
&approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
Some(&metadata),
|
||||
),
|
||||
"q".to_string(),
|
||||
mcp_tool_approval_prompt_options(
|
||||
Some(&session_key),
|
||||
Some(&persistent_key),
|
||||
/*tool_call_mcp_elicitation_enabled*/ false,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question;
|
||||
|
||||
assert_eq!(
|
||||
question
|
||||
@@ -695,16 +833,21 @@ fn codex_apps_tool_question_without_elicitation_omits_always_allow() {
|
||||
|
||||
#[test]
|
||||
fn custom_mcp_tool_question_offers_session_remember_and_always_allow() {
|
||||
let question = build_mcp_tool_approval_question(
|
||||
let question = mcp_tool_approval_prompt(
|
||||
&approval_prompt_request(
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
),
|
||||
"q".to_string(),
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*connector_name*/ None,
|
||||
prompt_options(
|
||||
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
|
||||
),
|
||||
/*question_override*/ None,
|
||||
);
|
||||
/*monitor_reason*/ None,
|
||||
)
|
||||
.expect("mcp approval prompt")
|
||||
.question;
|
||||
|
||||
assert_eq!(
|
||||
question
|
||||
@@ -1086,10 +1229,15 @@ fn accepted_elicitation_content_converts_to_request_user_input_response() {
|
||||
|
||||
#[test]
|
||||
fn approval_elicitation_meta_marks_tool_approvals() {
|
||||
let request = approval_prompt_request(
|
||||
"custom_server",
|
||||
"run_action",
|
||||
/*arguments*/ None,
|
||||
/*metadata*/ None,
|
||||
);
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
"custom_server",
|
||||
/*metadata*/ None,
|
||||
&request,
|
||||
/*tool_params*/ None,
|
||||
/*tool_params_display*/ None,
|
||||
prompt_options(
|
||||
@@ -1104,16 +1252,22 @@ fn approval_elicitation_meta_marks_tool_approvals() {
|
||||
|
||||
#[test]
|
||||
fn approval_elicitation_meta_merges_session_and_always_persist_for_custom_servers() {
|
||||
let metadata = approval_metadata(
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
/*connector_description*/ None,
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
);
|
||||
let request = approval_prompt_request(
|
||||
"custom_server",
|
||||
"run_action",
|
||||
Some(serde_json::json!({"id": 1})),
|
||||
Some(&metadata),
|
||||
);
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
"custom_server",
|
||||
Some(&approval_metadata(
|
||||
/*connector_id*/ None,
|
||||
/*connector_name*/ None,
|
||||
/*connector_description*/ None,
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
)),
|
||||
&request,
|
||||
Some(&serde_json::json!({"id": 1})),
|
||||
/*tool_params_display*/ None,
|
||||
prompt_options(
|
||||
@@ -1145,8 +1299,9 @@ fn guardian_mcp_review_request_includes_invocation_metadata() {
|
||||
})),
|
||||
};
|
||||
|
||||
let request = build_guardian_mcp_tool_review_request(
|
||||
let request = build_mcp_tool_approval_request(
|
||||
"call-1",
|
||||
"browser_navigate",
|
||||
&invocation,
|
||||
Some(&approval_metadata(
|
||||
Some("playwright"),
|
||||
@@ -1159,10 +1314,11 @@ fn guardian_mcp_review_request_includes_invocation_metadata() {
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
GuardianApprovalRequest::McpToolCall {
|
||||
ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
|
||||
tool_name: "browser_navigate".to_string(),
|
||||
hook_tool_name: "browser_navigate".to_string(),
|
||||
arguments: Some(serde_json::json!({
|
||||
"url": "https://example.com",
|
||||
})),
|
||||
@@ -1172,7 +1328,7 @@ fn guardian_mcp_review_request_includes_invocation_metadata() {
|
||||
tool_title: Some("Navigate".to_string()),
|
||||
tool_description: Some("Open a page".to_string()),
|
||||
annotations: None,
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1195,14 +1351,16 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
|
||||
openai_file_input_params: None,
|
||||
};
|
||||
|
||||
let request = build_guardian_mcp_tool_review_request("call-1", &invocation, Some(&metadata));
|
||||
let request =
|
||||
build_mcp_tool_approval_request("call-1", "dangerous_tool", &invocation, Some(&metadata));
|
||||
|
||||
assert_eq!(
|
||||
request,
|
||||
GuardianApprovalRequest::McpToolCall {
|
||||
ApprovalRequest::McpToolCall(McpToolCallApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
server: "custom_server".to_string(),
|
||||
tool_name: "dangerous_tool".to_string(),
|
||||
hook_tool_name: "dangerous_tool".to_string(),
|
||||
arguments: None,
|
||||
connector_id: None,
|
||||
connector_name: None,
|
||||
@@ -1214,7 +1372,7 @@ fn guardian_mcp_review_request_includes_annotations_when_present() {
|
||||
open_world_hint: Some(true),
|
||||
read_only_hint: Some(false),
|
||||
}),
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1316,16 +1474,24 @@ async fn guardian_review_decision_maps_to_mcp_tool_decision() {
|
||||
|
||||
#[test]
|
||||
fn approval_elicitation_meta_includes_connector_source_for_codex_apps() {
|
||||
let metadata = approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
);
|
||||
let request = approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
Some(serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
Some(&metadata),
|
||||
);
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
)),
|
||||
&request,
|
||||
Some(&serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
@@ -1351,16 +1517,24 @@ fn approval_elicitation_meta_includes_connector_source_for_codex_apps() {
|
||||
|
||||
#[test]
|
||||
fn approval_elicitation_meta_merges_session_and_always_persist_with_connector_source() {
|
||||
let metadata = approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
);
|
||||
let request = approval_prompt_request(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
"run_action",
|
||||
Some(serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
Some(&metadata),
|
||||
);
|
||||
assert_eq!(
|
||||
build_mcp_tool_approval_elicitation_meta(
|
||||
CODEX_APPS_MCP_SERVER_NAME,
|
||||
Some(&approval_metadata(
|
||||
Some("calendar"),
|
||||
Some("Calendar"),
|
||||
Some("Manage events and schedules."),
|
||||
Some("Run Action"),
|
||||
Some("Runs the selected action."),
|
||||
)),
|
||||
&request,
|
||||
Some(&serde_json::json!({
|
||||
"calendar_id": "primary",
|
||||
})),
|
||||
|
||||
@@ -14,6 +14,9 @@ use crate::agent::Mailbox;
|
||||
use crate::agent::MailboxReceiver;
|
||||
use crate::agent::agent_status_from_event;
|
||||
use crate::agent::status::is_final;
|
||||
use crate::approval_request::ApplyPatchApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::RequestPermissionsApprovalRequest;
|
||||
use crate::build_available_skills;
|
||||
use crate::commit_attribution::commit_message_trailer_instruction;
|
||||
use crate::compact;
|
||||
@@ -99,7 +102,6 @@ use codex_protocol::models::format_allow_prefixes;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::HasLegacyEvent;
|
||||
use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
@@ -116,7 +118,6 @@ use codex_protocol::protocol::W3cTraceContext;
|
||||
use codex_protocol::request_permissions::PermissionGrantScope;
|
||||
use codex_protocol::request_permissions::RequestPermissionProfile;
|
||||
use codex_protocol::request_permissions::RequestPermissionsArgs;
|
||||
use codex_protocol::request_permissions::RequestPermissionsEvent;
|
||||
use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
@@ -126,7 +127,6 @@ use codex_rollout_trace::AgentResultTracePayload;
|
||||
use codex_rollout_trace::ThreadStartedTraceMetadata;
|
||||
use codex_rollout_trace::ThreadTraceContext;
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
||||
use codex_shell_command::parse_command::parse_command;
|
||||
use codex_terminal_detection::user_agent;
|
||||
use codex_thread_store::CreateThreadParams;
|
||||
use codex_thread_store::LiveThread;
|
||||
@@ -1844,26 +1844,52 @@ impl Session {
|
||||
/// be used to derive the available decisions via
|
||||
/// [ExecApprovalRequestEvent::default_available_decisions].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn request_command_approval_for_request(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
approval_request: CommandApprovalRequest,
|
||||
approval_id: Option<String>,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
available_decisions: Option<Vec<ReviewDecision>>,
|
||||
) -> ReviewDecision {
|
||||
let proposed_network_policy_amendments = network_approval_context.as_ref().map(|context| {
|
||||
vec![
|
||||
NetworkPolicyAmendment {
|
||||
host: context.host.clone(),
|
||||
action: NetworkPolicyRuleAction::Allow,
|
||||
},
|
||||
NetworkPolicyAmendment {
|
||||
host: context.host.clone(),
|
||||
action: NetworkPolicyRuleAction::Deny,
|
||||
},
|
||||
]
|
||||
});
|
||||
let event = approval_request.exec_approval_event(
|
||||
turn_context.sub_id.clone(),
|
||||
approval_id,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
available_decisions,
|
||||
);
|
||||
self.request_exec_approval_event(turn_context, event).await
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
)]
|
||||
pub async fn request_command_approval(
|
||||
async fn request_exec_approval_event(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
available_decisions: Option<Vec<ReviewDecision>>,
|
||||
event: ExecApprovalRequestEvent,
|
||||
) -> ReviewDecision {
|
||||
// command-level approvals use `call_id`.
|
||||
// `approval_id` is only present for subcommand callbacks (execve intercept)
|
||||
let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
|
||||
let effective_approval_id = event.effective_approval_id();
|
||||
// Add the tx_approve callback to the map before sending the request.
|
||||
let (tx_approve, rx_approve) = oneshot::channel();
|
||||
let prev_entry = {
|
||||
@@ -1880,60 +1906,53 @@ impl Session {
|
||||
warn!("Overwriting existing pending approval for call_id: {effective_approval_id}");
|
||||
}
|
||||
|
||||
let parsed_cmd = parse_command(&command);
|
||||
let proposed_network_policy_amendments = network_approval_context.as_ref().map(|context| {
|
||||
vec![
|
||||
NetworkPolicyAmendment {
|
||||
host: context.host.clone(),
|
||||
action: NetworkPolicyRuleAction::Allow,
|
||||
},
|
||||
NetworkPolicyAmendment {
|
||||
host: context.host.clone(),
|
||||
action: NetworkPolicyRuleAction::Deny,
|
||||
},
|
||||
]
|
||||
});
|
||||
let available_decisions = available_decisions.unwrap_or_else(|| {
|
||||
let available_decisions = event.available_decisions.clone().unwrap_or_else(|| {
|
||||
ExecApprovalRequestEvent::default_available_decisions(
|
||||
network_approval_context.as_ref(),
|
||||
proposed_execpolicy_amendment.as_ref(),
|
||||
proposed_network_policy_amendments.as_deref(),
|
||||
additional_permissions.as_ref(),
|
||||
event.network_approval_context.as_ref(),
|
||||
event.proposed_execpolicy_amendment.as_ref(),
|
||||
event.proposed_network_policy_amendments.as_deref(),
|
||||
event.additional_permissions.as_ref(),
|
||||
)
|
||||
});
|
||||
let event = EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
call_id,
|
||||
approval_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
proposed_network_policy_amendments,
|
||||
additional_permissions,
|
||||
available_decisions: Some(available_decisions),
|
||||
parsed_cmd,
|
||||
});
|
||||
self.send_event(turn_context, event).await;
|
||||
self.send_event(
|
||||
turn_context,
|
||||
EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
|
||||
available_decisions: Some(available_decisions),
|
||||
..event
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
rx_approve.await.unwrap_or(ReviewDecision::Abort)
|
||||
}
|
||||
|
||||
pub async fn request_patch_approval_for_request(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
approval_request: ApplyPatchApprovalRequest,
|
||||
reason: Option<String>,
|
||||
grant_root: Option<PathBuf>,
|
||||
) -> oneshot::Receiver<ReviewDecision> {
|
||||
let event = approval_request.apply_patch_approval_event(
|
||||
turn_context.sub_id.clone(),
|
||||
reason,
|
||||
grant_root,
|
||||
);
|
||||
self.request_apply_patch_approval_event(turn_context, event)
|
||||
.await
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "active turn checks and turn state updates must remain atomic"
|
||||
)]
|
||||
pub async fn request_patch_approval(
|
||||
async fn request_apply_patch_approval_event(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: String,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
reason: Option<String>,
|
||||
grant_root: Option<PathBuf>,
|
||||
event: ApplyPatchApprovalRequestEvent,
|
||||
) -> oneshot::Receiver<ReviewDecision> {
|
||||
// Add the tx_approve callback to the map before sending the request.
|
||||
let (tx_approve, rx_approve) = oneshot::channel();
|
||||
let approval_id = call_id.clone();
|
||||
let approval_id = event.call_id.clone();
|
||||
let prev_entry = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
@@ -1948,14 +1967,8 @@ impl Session {
|
||||
warn!("Overwriting existing pending approval for call_id: {approval_id}");
|
||||
}
|
||||
|
||||
let event = EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
|
||||
call_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
changes,
|
||||
reason,
|
||||
grant_root,
|
||||
});
|
||||
self.send_event(turn_context, event).await;
|
||||
self.send_event(turn_context, EventMsg::ApplyPatchApprovalRequest(event))
|
||||
.await;
|
||||
rx_approve
|
||||
}
|
||||
|
||||
@@ -2012,6 +2025,13 @@ impl Session {
|
||||
}
|
||||
|
||||
let requested_permissions = args.permissions;
|
||||
let approval_request = RequestPermissionsApprovalRequest {
|
||||
id: call_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
reason: args.reason.clone(),
|
||||
permissions: requested_permissions.clone(),
|
||||
cwd: cwd.clone(),
|
||||
};
|
||||
|
||||
if crate::guardian::routes_approval_to_guardian(turn_context.as_ref()) {
|
||||
let originating_turn_state = {
|
||||
@@ -2021,17 +2041,11 @@ impl Session {
|
||||
let review_id = crate::guardian::new_guardian_review_id();
|
||||
let session = Arc::clone(self);
|
||||
let turn = Arc::clone(turn_context);
|
||||
let request = crate::guardian::GuardianApprovalRequest::RequestPermissions {
|
||||
id: call_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
reason: args.reason,
|
||||
permissions: requested_permissions.clone(),
|
||||
};
|
||||
let review_rx = crate::guardian::spawn_approval_request_review(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
request,
|
||||
approval_request.clone().into(),
|
||||
/*retry_reason*/ None,
|
||||
codex_analytics::GuardianApprovalRequestSource::MainTurn,
|
||||
cancellation_token.clone(),
|
||||
@@ -2111,14 +2125,9 @@ impl Session {
|
||||
warn!("Overwriting existing pending request_permissions for call_id: {call_id}");
|
||||
}
|
||||
|
||||
let event = EventMsg::RequestPermissions(RequestPermissionsEvent {
|
||||
call_id: call_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
reason: args.reason,
|
||||
permissions: requested_permissions,
|
||||
cwd: Some(cwd),
|
||||
});
|
||||
self.send_event(turn_context.as_ref(), event).await;
|
||||
let event = approval_request.request_permissions_event();
|
||||
self.send_event(turn_context.as_ref(), EventMsg::RequestPermissions(event))
|
||||
.await;
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancellation_token.cancelled() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianNetworkAccessTrigger;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::GuardianNetworkAccessTrigger;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
@@ -8,7 +8,6 @@ use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::hook_runtime::run_permission_request_hooks;
|
||||
use crate::network_policy_decision::denied_network_policy_message;
|
||||
use crate::session::session::Session;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_hooks::PermissionRequestDecision;
|
||||
use codex_network_proxy::BlockedRequest;
|
||||
@@ -460,17 +459,28 @@ impl NetworkApprovalService {
|
||||
protocol,
|
||||
};
|
||||
let guardian_approval_id = Self::approval_id_for_key(&key);
|
||||
let prompt_command = vec!["network-access".to_string(), target.clone()];
|
||||
let command = owner_call
|
||||
.as_ref()
|
||||
.map_or_else(|| prompt_command.join(" "), |call| call.command.clone());
|
||||
if let Some(permission_request_decision) = run_permission_request_hooks(
|
||||
&session,
|
||||
&turn_context,
|
||||
&guardian_approval_id,
|
||||
PermissionRequestPayload::bash(command, Some(format!("network-access {target}"))),
|
||||
)
|
||||
.await
|
||||
let command = owner_call.as_ref().map_or_else(
|
||||
|| format!("network-access {target}"),
|
||||
|call| call.command.clone(),
|
||||
);
|
||||
let approval_request = CommandApprovalRequest::NetworkAccess {
|
||||
id: guardian_approval_id.clone(),
|
||||
turn_id: owner_call
|
||||
.as_ref()
|
||||
.map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()),
|
||||
target: target.clone(),
|
||||
hook_command: command,
|
||||
cwd: turn_context.cwd.clone(),
|
||||
host: request.host.clone(),
|
||||
protocol,
|
||||
port: key.port,
|
||||
trigger: owner_call.as_ref().map(|call| call.trigger.clone()),
|
||||
};
|
||||
if let Some(permission_request_decision) =
|
||||
run_permission_request_hooks(&session, &turn_context, &guardian_approval_id, {
|
||||
approval_request.permission_request_payload()
|
||||
})
|
||||
.await
|
||||
{
|
||||
match permission_request_decision {
|
||||
PermissionRequestDecision::Allow => {
|
||||
@@ -503,33 +513,20 @@ impl NetworkApprovalService {
|
||||
&session,
|
||||
&turn_context,
|
||||
review_id,
|
||||
GuardianApprovalRequest::NetworkAccess {
|
||||
id: guardian_approval_id.clone(),
|
||||
turn_id: owner_call
|
||||
.as_ref()
|
||||
.map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()),
|
||||
target,
|
||||
host: request.host,
|
||||
protocol,
|
||||
port: key.port,
|
||||
trigger: owner_call.as_ref().map(|call| call.trigger.clone()),
|
||||
},
|
||||
approval_request.clone().into(),
|
||||
Some(policy_denial_message.clone()),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
.request_command_approval_for_request(
|
||||
turn_context.as_ref(),
|
||||
guardian_approval_id,
|
||||
approval_request,
|
||||
/*approval_id*/ None,
|
||||
prompt_command,
|
||||
turn_context.cwd.clone(),
|
||||
Some(prompt_reason),
|
||||
Some(network_approval_context.clone()),
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
/*additional_permissions*/ None,
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -394,8 +394,11 @@ impl ToolOrchestrator {
|
||||
where
|
||||
T: ToolRuntime<Rq, Out>,
|
||||
{
|
||||
let approval_request = tool.approval_request(req, &approval_ctx);
|
||||
if evaluate_permission_request_hooks
|
||||
&& let Some(permission_request) = tool.permission_request_payload(req)
|
||||
&& let Some(permission_request) = approval_request
|
||||
.as_ref()
|
||||
.and_then(crate::approval_request::ApprovalRequest::permission_request_payload)
|
||||
{
|
||||
match run_permission_request_hooks(
|
||||
approval_ctx.session,
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
//! Assumes `apply_patch` verification/approval happened upstream. Reuses the
|
||||
//! selected turn environment filesystem for both local and remote turns, with
|
||||
//! sandboxing enforced by the explicit filesystem sandbox context.
|
||||
use crate::approval_request::ApplyPatchApprovalRequest;
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::Sandboxable;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
@@ -53,14 +52,12 @@ impl ApplyPatchRuntime {
|
||||
Self
|
||||
}
|
||||
|
||||
fn build_guardian_review_request(
|
||||
req: &ApplyPatchRequest,
|
||||
call_id: &str,
|
||||
) -> GuardianApprovalRequest {
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
fn build_approval_request(req: &ApplyPatchRequest, call_id: &str) -> ApplyPatchApprovalRequest {
|
||||
ApplyPatchApprovalRequest {
|
||||
id: call_id.to_string(),
|
||||
cwd: req.action.cwd.clone(),
|
||||
files: req.file_paths.clone(),
|
||||
changes: req.changes.clone(),
|
||||
patch: req.action.patch.clone(),
|
||||
}
|
||||
}
|
||||
@@ -108,26 +105,29 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
) -> BoxFuture<'a, ReviewDecision> {
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let approval_keys = self.approval_keys(req);
|
||||
let changes = req.changes.clone();
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
let approval_request = Self::build_approval_request(req, ctx.call_id);
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id);
|
||||
return review_approval_request(session, turn, review_id, action, retry_reason)
|
||||
.await;
|
||||
return review_approval_request(
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
approval_request.clone().into(),
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if req.permissions_preapproved && retry_reason.is_none() {
|
||||
return ReviewDecision::Approved;
|
||||
}
|
||||
if let Some(reason) = retry_reason {
|
||||
let rx_approve = session
|
||||
.request_patch_approval(
|
||||
.request_patch_approval_for_request(
|
||||
turn,
|
||||
call_id,
|
||||
changes.clone(),
|
||||
approval_request.clone(),
|
||||
Some(reason),
|
||||
/*grant_root*/ None,
|
||||
)
|
||||
@@ -141,8 +141,11 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
approval_keys,
|
||||
|| async move {
|
||||
let rx_approve = session
|
||||
.request_patch_approval(
|
||||
turn, call_id, changes, /*reason*/ None, /*grant_root*/ None,
|
||||
.request_patch_approval_for_request(
|
||||
turn,
|
||||
approval_request.clone(),
|
||||
/*reason*/ None,
|
||||
/*grant_root*/ None,
|
||||
)
|
||||
.await;
|
||||
rx_approve.await.unwrap_or_default()
|
||||
@@ -173,14 +176,12 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
|
||||
Some(req.exec_approval_requirement.clone())
|
||||
}
|
||||
|
||||
fn permission_request_payload(
|
||||
fn approval_request(
|
||||
&self,
|
||||
req: &ApplyPatchRequest,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload {
|
||||
tool_name: HookToolName::apply_patch(),
|
||||
tool_input: serde_json::json!({ "command": req.action.patch }),
|
||||
})
|
||||
ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<ApprovalRequest> {
|
||||
Some(Self::build_approval_request(req, ctx.call_id).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::approval_request::ApplyPatchApprovalRequest;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
@@ -65,14 +66,15 @@ fn guardian_review_request_includes_patch_context() {
|
||||
permissions_preapproved: false,
|
||||
};
|
||||
|
||||
let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1");
|
||||
let guardian_request = ApplyPatchRuntime::build_approval_request(&request, "call-1");
|
||||
|
||||
assert_eq!(
|
||||
guardian_request,
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
ApplyPatchApprovalRequest {
|
||||
id: "call-1".to_string(),
|
||||
cwd: expected_cwd,
|
||||
files: request.file_paths,
|
||||
changes: request.changes,
|
||||
patch: expected_patch,
|
||||
}
|
||||
);
|
||||
@@ -80,7 +82,6 @@ fn guardian_review_request_includes_patch_context() {
|
||||
|
||||
#[test]
|
||||
fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
|
||||
let runtime = ApplyPatchRuntime::new();
|
||||
let path = std::env::temp_dir()
|
||||
.join("apply-patch-permission-request-payload.txt")
|
||||
.abs();
|
||||
@@ -98,9 +99,8 @@ fn permission_request_payload_uses_apply_patch_hook_name_and_aliases() {
|
||||
permissions_preapproved: false,
|
||||
};
|
||||
|
||||
let payload = runtime
|
||||
.permission_request_payload(&req)
|
||||
.expect("permission request payload");
|
||||
let payload =
|
||||
ApplyPatchRuntime::build_approval_request(&req, "call-1").permission_request_payload();
|
||||
|
||||
assert_eq!(payload.tool_name.name(), "apply_patch");
|
||||
assert_eq!(
|
||||
|
||||
@@ -8,10 +8,11 @@ builds sandbox transform inputs, and runs them under the current SandboxAttempt.
|
||||
pub(crate) mod unix_escalation;
|
||||
pub(crate) mod zsh_fork_backend;
|
||||
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::GuardianNetworkAccessTrigger;
|
||||
use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianNetworkAccessTrigger;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
@@ -25,7 +26,6 @@ use crate::tools::runtimes::maybe_wrap_shell_lc_with_snapshot;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::SandboxOverride;
|
||||
use crate::tools::sandboxing::Sandboxable;
|
||||
@@ -120,6 +120,18 @@ impl ShellRuntime {
|
||||
tx_event: ctx.session.get_tx_event(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_approval_request(req: &ShellRequest, call_id: String) -> CommandApprovalRequest {
|
||||
CommandApprovalRequest::Shell {
|
||||
id: call_id,
|
||||
command: req.command.clone(),
|
||||
hook_command: req.hook_command.clone(),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sandboxable for ShellRuntime {
|
||||
@@ -149,13 +161,11 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
ctx: ApprovalCtx<'a>,
|
||||
) -> BoxFuture<'a, ReviewDecision> {
|
||||
let keys = self.approval_keys(req);
|
||||
let command = req.command.clone();
|
||||
let cwd = req.cwd.clone();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let approval_request = Self::build_approval_request(req, ctx.call_id.to_string());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
@@ -163,14 +173,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: call_id,
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
},
|
||||
approval_request.clone().into(),
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
@@ -178,18 +181,15 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
with_cached_approval(&session.services, "shell", keys, move || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
.request_command_approval_for_request(
|
||||
turn,
|
||||
call_id,
|
||||
approval_request.clone(),
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
@@ -202,11 +202,12 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
Some(req.exec_approval_requirement.clone())
|
||||
}
|
||||
|
||||
fn permission_request_payload(&self, req: &ShellRequest) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload::bash(
|
||||
req.hook_command.clone(),
|
||||
req.justification.clone(),
|
||||
))
|
||||
fn approval_request(
|
||||
&self,
|
||||
req: &ShellRequest,
|
||||
ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<ApprovalRequest> {
|
||||
Some(Self::build_approval_request(req, ctx.call_id.to_string()).into())
|
||||
}
|
||||
|
||||
fn sandbox_mode_for_first_attempt(&self, req: &ShellRequest) -> SandboxOverride {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::ShellRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::exec::cancel_when_either;
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::guardian_rejection_message;
|
||||
use crate::guardian::guardian_timeout_message;
|
||||
use crate::guardian::new_guardian_review_id;
|
||||
@@ -16,7 +16,6 @@ use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::runtimes::build_sandbox_command;
|
||||
use crate::tools::runtimes::exec_env_for_sandbox_permissions;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
@@ -396,7 +395,6 @@ impl CoreShellActionProvider {
|
||||
stopwatch: &Stopwatch,
|
||||
additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
) -> anyhow::Result<PromptDecision> {
|
||||
let command = join_program_and_argv(program, argv);
|
||||
let workdir = workdir.clone();
|
||||
let session = self.session.clone();
|
||||
let turn = self.turn.clone();
|
||||
@@ -407,16 +405,20 @@ impl CoreShellActionProvider {
|
||||
Ok(stopwatch
|
||||
.pause_for(async move {
|
||||
// 1) Run PermissionRequest hooks
|
||||
let permission_request = PermissionRequestPayload::bash(
|
||||
codex_shell_command::parse_command::shlex_join(&command),
|
||||
/*description*/ None,
|
||||
);
|
||||
let effective_approval_id = approval_id.clone().unwrap_or_else(|| call_id.clone());
|
||||
let approval_request = CommandApprovalRequest::Execve {
|
||||
id: call_id.clone(),
|
||||
source,
|
||||
program: program.to_string_lossy().into_owned(),
|
||||
argv: argv.to_vec(),
|
||||
cwd: workdir.clone(),
|
||||
additional_permissions: additional_permissions.clone(),
|
||||
};
|
||||
match run_permission_request_hooks(
|
||||
&session,
|
||||
&turn,
|
||||
&effective_approval_id,
|
||||
permission_request,
|
||||
approval_request.permission_request_payload(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -443,14 +445,7 @@ impl CoreShellActionProvider {
|
||||
&session,
|
||||
&turn,
|
||||
review_id.clone(),
|
||||
GuardianApprovalRequest::Execve {
|
||||
id: call_id.clone(),
|
||||
source,
|
||||
program: program.to_string_lossy().into_owned(),
|
||||
argv: argv.to_vec(),
|
||||
cwd: workdir.clone(),
|
||||
additional_permissions,
|
||||
},
|
||||
approval_request.clone().into(),
|
||||
/*retry_reason*/ None,
|
||||
)
|
||||
.await;
|
||||
@@ -463,16 +458,13 @@ impl CoreShellActionProvider {
|
||||
|
||||
// 3) Fall back to regular user prompt
|
||||
let decision = session
|
||||
.request_command_approval(
|
||||
.request_command_approval_for_request(
|
||||
&turn,
|
||||
call_id,
|
||||
approval_request,
|
||||
approval_id,
|
||||
command,
|
||||
workdir.clone(),
|
||||
/*reason*/ None,
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
additional_permissions,
|
||||
Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -4,11 +4,12 @@ Runtime: unified exec
|
||||
Handles approval + sandbox orchestration for unified exec requests, delegating to
|
||||
the process manager to spawn PTYs once an ExecRequest is prepared.
|
||||
*/
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::approval_request::CommandApprovalRequest;
|
||||
use crate::approval_request::GuardianNetworkAccessTrigger;
|
||||
use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecCapturePolicy;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::GuardianNetworkAccessTrigger;
|
||||
use crate::guardian::review_approval_request;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::ExecServerEnvConfig;
|
||||
@@ -23,7 +24,6 @@ use crate::tools::runtimes::shell::zsh_fork_backend;
|
||||
use crate::tools::sandboxing::Approvable;
|
||||
use crate::tools::sandboxing::ApprovalCtx;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::tools::sandboxing::PermissionRequestPayload;
|
||||
use crate::tools::sandboxing::SandboxAttempt;
|
||||
use crate::tools::sandboxing::SandboxOverride;
|
||||
use crate::tools::sandboxing::Sandboxable;
|
||||
@@ -110,6 +110,19 @@ impl<'a> UnifiedExecRuntime<'a> {
|
||||
shell_mode,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_approval_request(req: &UnifiedExecRequest, call_id: String) -> CommandApprovalRequest {
|
||||
CommandApprovalRequest::ExecCommand {
|
||||
id: call_id,
|
||||
command: req.command.clone(),
|
||||
hook_command: req.hook_command.clone(),
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
tty: req.tty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sandboxable for UnifiedExecRuntime<'_> {
|
||||
@@ -143,11 +156,9 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
let keys = self.approval_keys(req);
|
||||
let session = ctx.session;
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let command = req.command.clone();
|
||||
let cwd = req.cwd.clone();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let approval_request = Self::build_approval_request(req, ctx.call_id.to_string());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
@@ -155,15 +166,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
session,
|
||||
turn,
|
||||
review_id,
|
||||
GuardianApprovalRequest::ExecCommand {
|
||||
id: call_id,
|
||||
command,
|
||||
cwd: cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
tty: req.tty,
|
||||
},
|
||||
approval_request.clone().into(),
|
||||
retry_reason,
|
||||
)
|
||||
.await;
|
||||
@@ -171,18 +174,15 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
with_cached_approval(&session.services, "unified_exec", keys, || async move {
|
||||
let available_decisions = None;
|
||||
session
|
||||
.request_command_approval(
|
||||
.request_command_approval_for_request(
|
||||
turn,
|
||||
call_id,
|
||||
approval_request.clone(),
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd.clone(),
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
.proposed_execpolicy_amendment()
|
||||
.cloned(),
|
||||
req.additional_permissions.clone(),
|
||||
available_decisions,
|
||||
)
|
||||
.await
|
||||
@@ -198,14 +198,12 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
Some(req.exec_approval_requirement.clone())
|
||||
}
|
||||
|
||||
fn permission_request_payload(
|
||||
fn approval_request(
|
||||
&self,
|
||||
req: &UnifiedExecRequest,
|
||||
) -> Option<PermissionRequestPayload> {
|
||||
Some(PermissionRequestPayload::bash(
|
||||
req.hook_command.clone(),
|
||||
req.justification.clone(),
|
||||
))
|
||||
ctx: &ApprovalCtx<'_>,
|
||||
) -> Option<ApprovalRequest> {
|
||||
Some(Self::build_approval_request(req, ctx.call_id.to_string()).into())
|
||||
}
|
||||
|
||||
fn sandbox_mode_for_first_attempt(&self, req: &UnifiedExecRequest) -> SandboxOverride {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//! `ApprovalCtx`, `Approvable`) together with the sandbox orchestration traits
|
||||
//! and helpers (`Sandboxable`, `ToolRuntime`, `SandboxAttempt`, etc.).
|
||||
|
||||
use crate::approval_request::ApprovalRequest;
|
||||
use crate::sandboxing::ExecOptions;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
@@ -309,9 +310,9 @@ pub(crate) trait Approvable<Req> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Return hook input for approval-time policy hooks when this runtime wants
|
||||
/// hook evaluation to run before guardian or user approval.
|
||||
fn permission_request_payload(&self, _req: &Req) -> Option<PermissionRequestPayload> {
|
||||
/// Return the canonical approval action for this request when the runtime
|
||||
/// participates in approval hooks and/or guardian review.
|
||||
fn approval_request(&self, _req: &Req, _ctx: &ApprovalCtx<'_>) -> Option<ApprovalRequest> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user