mirror of
https://github.com/openai/codex.git
synced 2026-10-21 14:23:14 +00:00
Test remote Guardian network approval scoping (#36177)
## What changed Add integration coverage for managed network requests that verifies: - Guardian allow and deny decisions apply only to the triggering request. - A decision for a remote environment does not carry over to a local one. - A user-granted remote session approval remains active after Guardian-reviewed requests. - Guardian assessments receive the exact network action and execution environment for each request. GitOrigin-RevId: 6701074c6d60b9f28a6d47ebaf910bb1e9f2fa07
This commit is contained in:
@@ -1136,6 +1136,225 @@ async fn guardian_receives_exact_trigger_for_single_network_request() -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_guardian_network_decisions_are_scoped_to_each_request_and_environment() -> Result<()>
|
||||
{
|
||||
skip_if_target_windows!(Ok(()), "uses the POSIX/Python network fixture");
|
||||
skip_if_host_windows!(Ok(()));
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
skip_if_no_remote_env!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let test = managed_network_unified_exec_test(&server).await?;
|
||||
let remote = test.executor_environment().selection().clone();
|
||||
assert_eq!(remote.environment_id, REMOTE_ENVIRONMENT_ID);
|
||||
let session_call_id = "remote-guardian-session-approval";
|
||||
let session_command = remote_network_proxy_request_command("REMOTE_GUARDIAN_SESSION");
|
||||
let session_probe_call_id = "remote-guardian-session-probe";
|
||||
let session_probe_command =
|
||||
remote_network_proxy_request_command("REMOTE_GUARDIAN_SESSION_PROBE");
|
||||
|
||||
let cases = [
|
||||
(
|
||||
"remote-guardian-deny",
|
||||
remote_network_proxy_request_command("REMOTE_GUARDIAN_DENY"),
|
||||
remote.clone(),
|
||||
"The first remote request must be denied.",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"remote-guardian-allow",
|
||||
remote_network_proxy_request_command("REMOTE_GUARDIAN_ALLOW"),
|
||||
remote.clone(),
|
||||
"This remote request is safe to allow once.",
|
||||
true,
|
||||
),
|
||||
(
|
||||
"remote-guardian-deny-again",
|
||||
remote_network_proxy_request_command("REMOTE_GUARDIAN_DENY_AGAIN"),
|
||||
remote.clone(),
|
||||
"A previous remote approval must not approve a later request.",
|
||||
false,
|
||||
),
|
||||
(
|
||||
"local-guardian-deny",
|
||||
remote_network_proxy_request_command("LOCAL_GUARDIAN_DENY"),
|
||||
local(test.cwd.path().abs()),
|
||||
"A remote approval must not approve the local environment.",
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
let mut scripted_responses = Vec::with_capacity(cases.len() * 3 + 4);
|
||||
for (call_id, command, _, rationale, approved) in &cases {
|
||||
if *call_id == "local-guardian-deny" {
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created("resp-remote-guardian-session-approval-parent"),
|
||||
ev_function_call(
|
||||
session_call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&network_exec_args(&session_command))?,
|
||||
),
|
||||
ev_completed("resp-remote-guardian-session-approval-parent"),
|
||||
]));
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created("resp-remote-guardian-session-approval-done"),
|
||||
ev_assistant_message("msg-remote-guardian-session-approval-done", "done"),
|
||||
ev_completed("resp-remote-guardian-session-approval-done"),
|
||||
]));
|
||||
}
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created(&format!("resp-{call_id}-parent")),
|
||||
ev_function_call(
|
||||
call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&network_exec_args(command))?,
|
||||
),
|
||||
ev_completed(&format!("resp-{call_id}-parent")),
|
||||
]));
|
||||
let assessment = json!({
|
||||
"risk_level": if *approved { "low" } else { "high" },
|
||||
"user_authorization": if *approved { "high" } else { "low" },
|
||||
"outcome": if *approved { "allow" } else { "deny" },
|
||||
"rationale": rationale,
|
||||
});
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created(&format!("resp-{call_id}-guardian")),
|
||||
ev_assistant_message(&format!("msg-{call_id}-guardian"), &assessment.to_string()),
|
||||
ev_completed(&format!("resp-{call_id}-guardian")),
|
||||
]));
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created(&format!("resp-{call_id}-done")),
|
||||
ev_assistant_message(&format!("msg-{call_id}-done"), "done"),
|
||||
ev_completed(&format!("resp-{call_id}-done")),
|
||||
]));
|
||||
}
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created("resp-remote-guardian-session-probe-parent"),
|
||||
ev_function_call(
|
||||
session_probe_call_id,
|
||||
"exec_command",
|
||||
&serde_json::to_string(&network_exec_args(&session_probe_command))?,
|
||||
),
|
||||
ev_completed("resp-remote-guardian-session-probe-parent"),
|
||||
]));
|
||||
scripted_responses.push(sse(vec![
|
||||
ev_response_created("resp-remote-guardian-session-probe-done"),
|
||||
ev_assistant_message("msg-remote-guardian-session-probe-done", "done"),
|
||||
ev_completed("resp-remote-guardian-session-probe-done"),
|
||||
]));
|
||||
let responses = mount_sse_sequence(&server, scripted_responses).await;
|
||||
|
||||
for (call_id, _, environment, _, _) in &cases {
|
||||
if *call_id == "local-guardian-deny" {
|
||||
submit_managed_network_turn(
|
||||
&test,
|
||||
"approve the remote destination for this session",
|
||||
vec![remote.clone()],
|
||||
ApprovalsReviewer::User,
|
||||
AskForApproval::OnRequest,
|
||||
)
|
||||
.await?;
|
||||
let approval = expect_network_approval(&test, REMOTE_ENVIRONMENT_ID).await?;
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id: approval.effective_approval_id(),
|
||||
turn_id: Some(approval.turn_id),
|
||||
decision: ReviewDecision::ApprovedForSession,
|
||||
})
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(15), wait_for_turn_complete(&test))
|
||||
.await
|
||||
.context("remote session approval should complete")?;
|
||||
}
|
||||
let prompt = format!("review network request {call_id}");
|
||||
submit_managed_network_turn(
|
||||
&test,
|
||||
&prompt,
|
||||
vec![environment.clone()],
|
||||
ApprovalsReviewer::AutoReview,
|
||||
AskForApproval::OnRequest,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(15),
|
||||
wait_for_completion_without_network_prompt(&test),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Guardian review for {call_id} should complete"))?;
|
||||
}
|
||||
submit_managed_network_turn(
|
||||
&test,
|
||||
"verify the remote session approval remains active",
|
||||
vec![remote],
|
||||
ApprovalsReviewer::User,
|
||||
AskForApproval::OnRequest,
|
||||
)
|
||||
.await?;
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(15),
|
||||
wait_for_completion_without_network_prompt(&test),
|
||||
)
|
||||
.await
|
||||
.context("remote session approval should bypass another network prompt")?;
|
||||
|
||||
let mut expected_actions = Vec::with_capacity(cases.len());
|
||||
for (call_id, command, environment, _, _) in &cases {
|
||||
let cwd = environment
|
||||
.cwd
|
||||
.to_abs_path()
|
||||
.with_context(|| format!("resolve the environment cwd for {call_id}"))?;
|
||||
expected_actions.push(json!({
|
||||
"host": NETWORK_TEST_HOST,
|
||||
"port": 80,
|
||||
"protocol": "http",
|
||||
"target": NETWORK_TEST_TARGET,
|
||||
"tool": "network_access",
|
||||
"trigger": {
|
||||
"callId": call_id,
|
||||
"command": ["/bin/sh", "-c", command],
|
||||
"cwd": cwd,
|
||||
"sandboxPermissions": "use_default",
|
||||
"toolName": "exec_command",
|
||||
"tty": false,
|
||||
},
|
||||
}));
|
||||
}
|
||||
assert_eq!(guardian_network_actions(&responses)?, expected_actions);
|
||||
|
||||
let requests = responses.requests();
|
||||
for (call_id, marker) in [
|
||||
(session_call_id, "REMOTE_GUARDIAN_SESSION:HTTP/1.1 502"),
|
||||
(
|
||||
session_probe_call_id,
|
||||
"REMOTE_GUARDIAN_SESSION_PROBE:HTTP/1.1 502",
|
||||
),
|
||||
] {
|
||||
let output = requests
|
||||
.iter()
|
||||
.find_map(|request| request.function_call_output_text(call_id))
|
||||
.with_context(|| format!("expected remote session network output for {call_id}"))?;
|
||||
assert!(output.contains(marker));
|
||||
assert!(!output.contains("rejected"));
|
||||
}
|
||||
for (call_id, _, _, rationale, approved) in &cases {
|
||||
let output = requests
|
||||
.iter()
|
||||
.find_map(|request| request.function_call_output_text(call_id))
|
||||
.with_context(|| format!("expected network tool output for {call_id}"))?;
|
||||
if *approved {
|
||||
assert!(output.contains("REMOTE_GUARDIAN_ALLOW:HTTP/1.1 502"));
|
||||
assert!(!output.contains("rejected"));
|
||||
} else {
|
||||
assert!(output.contains(rationale));
|
||||
assert!(!output.contains("HTTP/1.1 502"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn approved_network_host_for_one_environment_still_prompts_in_another() -> Result<()> {
|
||||
skip_if_target_windows!(Ok(()), "uses the POSIX/Python network fixture");
|
||||
@@ -1332,6 +1551,13 @@ fn network_exec_args(command: &str) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_network_proxy_request_command(marker: &str) -> String {
|
||||
let host = NETWORK_TEST_HOST;
|
||||
format!(
|
||||
"python3 -c \"import os,socket,urllib.parse; proxy = urllib.parse.urlparse(os.environ['HTTP_PROXY']); sock = socket.create_connection((proxy.hostname, proxy.port), timeout=10); sock.sendall(b'GET http://{host} HTTP/1.1\\r\\nHost: {host}\\r\\nConnection: close\\r\\n\\r\\n'); print('{marker}:' + sock.makefile('rb').readline().decode(errors='replace'))\""
|
||||
)
|
||||
}
|
||||
|
||||
async fn submit_managed_network_turn(
|
||||
test: &TestCodex,
|
||||
prompt: &str,
|
||||
|
||||
Reference in New Issue
Block a user