mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
## Why Guardian approval actions can contain paths from a remote executor whose path convention differs from the host, such as Windows paths reviewed on a POSIX host. Converting those paths to host-native absolute paths prevents the action from being reviewed. ## What changed - Render command working directories using the executor's reported path convention, and preserve URI-backed paths for attribution. - Carry foreign command and patch paths through Guardian events and app-server schemas as legacy path strings. - Bound remote plugin attribution within the overall review deadline and reject Guardian action payloads that exceed the review byte limit. ## Testing - Cover Windows and POSIX executor path rendering, foreign patch paths, protocol round trips, oversized payload rejection, and remote Guardian review context. GitOrigin-RevId: 38be0fcdac897139519e74e077eb5cb4f8f8017e
135 lines
4.7 KiB
Rust
135 lines
4.7 KiB
Rust
use super::*;
|
|
use crate::session::tests::make_session_and_context_with_rx;
|
|
use codex_models_manager::model_info::model_info_from_slug;
|
|
use codex_protocol::approvals::NetworkPolicyAmendment;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn approval_resolution_rejects_denied_network_policy_amendment() {
|
|
let resolution = ApprovalResolution {
|
|
decision: ReviewDecision::NetworkPolicyAmendment {
|
|
network_policy_amendment: NetworkPolicyAmendment {
|
|
host: "denied.example.com".to_string(),
|
|
action: NetworkPolicyRuleAction::Deny,
|
|
},
|
|
},
|
|
source: ApprovalResolutionSource::User,
|
|
};
|
|
|
|
assert!(matches!(
|
|
resolution.into_tool_result(&model_info_from_slug("acting-model")),
|
|
Err(ToolError::Rejected(rejection)) if rejection == "rejected by user"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn approval_resolution_rejects_mcp_policy_amendment() {
|
|
let resolution = ApprovalResolution {
|
|
decision: ReviewDecision::ApprovedMcpPolicyAmendment,
|
|
source: ApprovalResolutionSource::User,
|
|
};
|
|
|
|
assert!(matches!(
|
|
resolution.into_tool_result(&model_info_from_slug("acting-model")),
|
|
Err(ToolError::Rejected(rejection)) if rejection == "Error while requesting approval"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn approval_resolution_aborts_turn_when_approval_is_aborted() {
|
|
let resolution = ApprovalResolution {
|
|
decision: ReviewDecision::Abort,
|
|
source: ApprovalResolutionSource::User,
|
|
};
|
|
|
|
assert!(matches!(
|
|
resolution.into_tool_result(&model_info_from_slug("acting-model")),
|
|
Err(ToolError::Codex(error))
|
|
if matches!(
|
|
error.details(),
|
|
codex_protocol::error::CodexErrorDetails::TurnAborted
|
|
)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn approval_resolution_uses_acting_model_timeout_instructions() {
|
|
let mut model = model_info_from_slug("acting-model");
|
|
for timeout_instructions in ["Catalog timeout instructions.", ""] {
|
|
model.model_messages = Some(
|
|
serde_json::from_value(serde_json::json!({
|
|
"auto_review": {
|
|
"timeout_instructions": timeout_instructions,
|
|
},
|
|
}))
|
|
.expect("model messages should deserialize"),
|
|
);
|
|
let resolution = ApprovalResolution {
|
|
decision: ReviewDecision::TimedOut,
|
|
source: ApprovalResolutionSource::Guardian,
|
|
};
|
|
|
|
assert!(matches!(
|
|
resolution.into_tool_result(&model),
|
|
Err(ToolError::Rejected(rejection)) if rejection == timeout_instructions
|
|
));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn explicit_mcp_reviewer_override_takes_precedence_over_action_context() {
|
|
let (session, turn, events) = make_session_and_context_with_rx().await;
|
|
let action = ApprovalAction::McpToolCall {
|
|
id: "mcp-override".to_string(),
|
|
server: "example".to_string(),
|
|
tool_name: "dangerous".to_string(),
|
|
arguments: None,
|
|
connector_id: None,
|
|
connector_name: None,
|
|
connector_description: None,
|
|
connected_account_email: None,
|
|
tool_title: None,
|
|
tool_description: None,
|
|
annotations: None,
|
|
hook_tool_name: HookToolName::new("mcp__example__dangerous"),
|
|
approval_policy: AskForApproval::OnRequest,
|
|
reviewer: ApprovalsReviewer::User,
|
|
approval_mode: AppToolApproval::Prompt,
|
|
allow_session_remember: false,
|
|
allow_persistent_approval: false,
|
|
};
|
|
let mut review_context = GuardianReviewContext::from(&turn);
|
|
review_context.approval_policy = AskForApproval::OnRequest;
|
|
review_context.approvals_reviewer = ApprovalsReviewer::AutoReview;
|
|
let context = ApprovalContext {
|
|
review_context,
|
|
cancellation_token: None,
|
|
call_id: "mcp-override".to_string(),
|
|
tool_name: ToolName::plain("dangerous"),
|
|
strict_auto_review: false,
|
|
approval_reason: None,
|
|
retry_reason: None,
|
|
network_approval_context: None,
|
|
};
|
|
|
|
tokio::select! {
|
|
resolution = session.request_reviewer_approval(action, &context) => {
|
|
panic!("expected a user approval request, got {resolution:?}");
|
|
}
|
|
event = events.recv() => {
|
|
let codex_protocol::protocol::EventMsg::ElicitationRequest(request) =
|
|
event.expect("receive user approval request").msg
|
|
else {
|
|
panic!("expected an MCP user approval request");
|
|
};
|
|
assert_eq!(request.server_name, "example");
|
|
assert_eq!(
|
|
request.id,
|
|
codex_protocol::mcp::RequestId::String(
|
|
"mcp_tool_call_approval_mcp-override".to_string()
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|