Require explicit sandbox permissions with shell justifications (#36350)

## What changed

Reject `shell_command` and unified `exec_command` calls that provide a
`justification` without also specifying `sandbox_permissions`. The
model-visible error tells callers to explicitly request `require_escalated`
execution or omit the justification.

## Testing

- Add unit coverage for `shell_command` argument validation.
- Add integration coverage for both shell tool paths, including verification
  that rejected unified exec calls do not begin execution.

GitOrigin-RevId: 6af39d2c889af492440615eae12085b99a8b5169
This commit is contained in:
Mitsuhiro Kotake authored and copyberry committed 2026-07-31 15:34:03 +00:00
1 parent 3d1d26915a
commit 7b38c48da9
7 files changed
+178 -3

No files matched your search

+72
View File
@@ -385,6 +385,78 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_rejects_justification_without_sandbox_permissions() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex().with_model("gpt-5.2").with_config(|config| {
config.use_experimental_unified_exec_tool = true;
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
});
let test = builder.build_with_auto_env(&server).await?;
let call_id = "uexec-missing-sandbox-permissions";
let args = json!({
"cmd": "echo should not run",
"justification": "Allow this command",
});
let mock = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
ev_function_call(call_id, "exec_command", &serde_json::to_string(&args)?),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "finished"),
ev_completed("resp-2"),
]),
],
)
.await;
submit_unified_exec_turn(
&test,
"run the command with escalation",
PermissionProfile::Disabled,
)
.await?;
let mut saw_exec_begin = false;
wait_for_event(&test.codex, |event| match event {
EventMsg::ExecCommandBegin(event) if event.call_id == call_id => {
saw_exec_begin = true;
false
}
EventMsg::TurnComplete(_) => true,
_ => false,
})
.await;
assert!(
!saw_exec_begin,
"rejected exec_command should not emit ExecCommandBegin"
);
let request = mock
.last_request()
.expect("model should receive the rejected tool call output");
let output_item = request.function_call_output(call_id);
let output = extract_output_text(&output_item)
.expect("rejected tool call should include model-visible text");
assert_eq!(
output,
"`justification` requires an explicit `sandbox_permissions`; use `sandbox_permissions: \"require_escalated\"` for unsandboxed execution, or omit `justification`."
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
// TODO(anp): Remove after unified-exec fixtures use target-native commands.