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:
jif
2026-07-30 15:50:04 +00:00
committed by copyberry
parent 578c1b2230
commit 355d2a802a
@@ -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,