mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: handle deferred network proxy denials (#19184)
## Why This bug is exposed by Guardian/auto-review approvals. With the managed network proxy enabled, a blocked network request can be reported back through the network approval service as an approval denial after the command has already started. Before this change, the shell and unified exec runtimes registered those network approval calls, but did not have a way to observe an async proxy denial as a cancellation/failure signal for the running process. The result was confusing: Guardian/auto-review could correctly deny network access, but the command path could keep running or unregister the approval without surfacing the denial as the command failure. ## What Changed - `NetworkApprovalService` now attaches a cancellation token to active and deferred network approvals. - Proxy-denial outcomes are recorded only for active registrations, cancel the owning token, and are consumed when the approval is finalized. - The shell runtime combines the normal command timeout with the network-denial cancellation token. - Unified exec stores the deferred network approval object, terminates tracked processes when the proxy denial arrives, and returns the denial as a process failure while polling or completing the process. - Tool orchestration passes the active network approval cancellation token into the sandbox attempt and preserves deferred approval errors instead of silently unregistering them. - App-server `command/exec` now handles the combined timeout-or-cancellation expiration variant used by the runtime. ## Verification - `cargo test -p codex-core network_approval --lib` - `cargo clippy -p codex-app-server --all-targets -- -D warnings` - `cargo clippy -p codex-core --all-targets -- -D warnings` --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
73cd831952
commit
07c8b8c77c
@@ -132,6 +132,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
cwd,
|
||||
Some(process_id.to_string()),
|
||||
transcript,
|
||||
String::new(),
|
||||
message,
|
||||
duration,
|
||||
)
|
||||
@@ -238,10 +239,15 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
|
||||
cwd: AbsolutePathBuf,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
message: String,
|
||||
duration: Duration,
|
||||
) {
|
||||
let stdout = resolve_aggregated_output(&transcript, String::new()).await;
|
||||
let stdout = if fallback_output.is_empty() {
|
||||
resolve_aggregated_output(&transcript, fallback_output).await
|
||||
} else {
|
||||
fallback_output
|
||||
};
|
||||
let aggregated_output = if stdout.is_empty() {
|
||||
message.clone()
|
||||
} else {
|
||||
|
||||
@@ -37,6 +37,7 @@ use tokio::sync::Mutex;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
|
||||
mod async_watcher;
|
||||
mod errors;
|
||||
@@ -150,7 +151,7 @@ struct ProcessEntry {
|
||||
process_id: i32,
|
||||
hook_command: String,
|
||||
tty: bool,
|
||||
network_approval_id: Option<String>,
|
||||
network_approval: Option<DeferredNetworkApproval>,
|
||||
session: Weak<Session>,
|
||||
last_used: tokio::time::Instant,
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ async fn exec_command_with_tty(
|
||||
process_id,
|
||||
hook_command: cmd.to_string(),
|
||||
tty,
|
||||
network_approval_id: None,
|
||||
network_approval: None,
|
||||
session: Arc::downgrade(session),
|
||||
last_used: started_at,
|
||||
};
|
||||
|
||||
@@ -212,6 +212,14 @@ impl UnifiedExecProcess {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn fail_and_terminate(&self, message: String) {
|
||||
let state = self.state_rx.borrow().clone();
|
||||
if state.failure_message.is_none() {
|
||||
let _ = self.state_tx.send_replace(state.failed(message));
|
||||
}
|
||||
self.terminate();
|
||||
}
|
||||
|
||||
async fn snapshot_output(&self) -> Vec<Vec<u8>> {
|
||||
let guard = self.output_buffer.lock().await;
|
||||
guard.snapshot_chunks()
|
||||
|
||||
@@ -69,6 +69,9 @@ const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [
|
||||
("GH_PAGER", "cat"),
|
||||
("CODEX_CI", "1"),
|
||||
];
|
||||
const NETWORK_ACCESS_DENIED_MESSAGE: &str =
|
||||
"Network access was denied by the Codex sandbox network proxy.";
|
||||
const LATE_NETWORK_DENIAL_GRACE_PERIOD: Duration = Duration::from_millis(100);
|
||||
|
||||
/// Test-only override for deterministic unified exec process IDs.
|
||||
///
|
||||
@@ -169,6 +172,8 @@ struct PreparedProcessHandles {
|
||||
output_closed_notify: Arc<Notify>,
|
||||
cancellation_token: CancellationToken,
|
||||
pause_state: Option<watch::Receiver<bool>>,
|
||||
session: Option<Arc<crate::session::session::Session>>,
|
||||
network_approval: Option<DeferredNetworkApproval>,
|
||||
hook_command: String,
|
||||
process_id: i32,
|
||||
tty: bool,
|
||||
@@ -178,6 +183,151 @@ fn exec_server_process_id(process_id: i32) -> String {
|
||||
process_id.to_string()
|
||||
}
|
||||
|
||||
async fn unregister_network_approval_for_entry(entry: &ProcessEntry) {
|
||||
if let Some(network_approval) = entry.network_approval.as_ref()
|
||||
&& let Some(session) = entry.session.upgrade()
|
||||
{
|
||||
session
|
||||
.services
|
||||
.network_approval
|
||||
.unregister_call(network_approval.registration_id())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn finish_network_approval_after_process_exit_for_entry(
|
||||
entry: &ProcessEntry,
|
||||
) -> Result<(), String> {
|
||||
let session = entry.session.upgrade();
|
||||
finish_deferred_network_approval_after_process_exit_for_session(
|
||||
session.as_ref(),
|
||||
entry.network_approval.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn finish_deferred_network_approval_for_session(
|
||||
session: Option<&Arc<crate::session::session::Session>>,
|
||||
deferred: Option<DeferredNetworkApproval>,
|
||||
) -> Result<(), String> {
|
||||
let Some(session) = session else {
|
||||
return Ok(());
|
||||
};
|
||||
finish_deferred_network_approval(session.as_ref(), deferred)
|
||||
.await
|
||||
.map_err(network_approval_error_message)
|
||||
}
|
||||
|
||||
fn network_approval_error_message(err: ToolError) -> String {
|
||||
match err {
|
||||
ToolError::Rejected(message) => message,
|
||||
ToolError::Codex(err) => err.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn network_denial_message_for_session(
|
||||
session: Option<&Arc<crate::session::session::Session>>,
|
||||
deferred: Option<DeferredNetworkApproval>,
|
||||
) -> String {
|
||||
let Some(session) = session else {
|
||||
return NETWORK_ACCESS_DENIED_MESSAGE.to_string();
|
||||
};
|
||||
match finish_deferred_network_approval(session.as_ref(), deferred).await {
|
||||
Ok(()) => NETWORK_ACCESS_DENIED_MESSAGE.to_string(),
|
||||
Err(err) => network_approval_error_message(err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_late_network_denial(network_cancelled: Option<CancellationToken>) -> bool {
|
||||
let Some(network_cancelled) = network_cancelled else {
|
||||
return false;
|
||||
};
|
||||
if network_cancelled.is_cancelled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = network_cancelled.cancelled() => true,
|
||||
_ = tokio::time::sleep(LATE_NETWORK_DENIAL_GRACE_PERIOD) => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn finish_deferred_network_approval_after_process_exit_for_session(
|
||||
session: Option<&Arc<crate::session::session::Session>>,
|
||||
deferred: Option<DeferredNetworkApproval>,
|
||||
) -> Result<(), String> {
|
||||
wait_for_late_network_denial(
|
||||
deferred
|
||||
.as_ref()
|
||||
.map(DeferredNetworkApproval::cancellation_token),
|
||||
)
|
||||
.await;
|
||||
finish_deferred_network_approval_for_session(session, deferred).await
|
||||
}
|
||||
|
||||
fn fail_process_with_message(process: &UnifiedExecProcess, message: String) -> UnifiedExecError {
|
||||
if let Some(message) = process.failure_message() {
|
||||
process.terminate();
|
||||
return UnifiedExecError::process_failed(message);
|
||||
}
|
||||
|
||||
process.fail_and_terminate(message.clone());
|
||||
UnifiedExecError::process_failed(process.failure_message().unwrap_or(message))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn emit_failed_initial_exec_end_if_unstored(
|
||||
process_started_alive: bool,
|
||||
context: &UnifiedExecContext,
|
||||
request: &ExecCommandRequest,
|
||||
cwd: AbsolutePathBuf,
|
||||
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
message: String,
|
||||
wall_time: Duration,
|
||||
) {
|
||||
if process_started_alive {
|
||||
return;
|
||||
}
|
||||
|
||||
emit_failed_exec_end_for_unified_exec(
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
cwd,
|
||||
Some(request.process_id.to_string()),
|
||||
transcript,
|
||||
fallback_output,
|
||||
message,
|
||||
wall_time,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn terminate_process_on_network_denial(
|
||||
process: Arc<UnifiedExecProcess>,
|
||||
session: std::sync::Weak<crate::session::session::Session>,
|
||||
deferred: DeferredNetworkApproval,
|
||||
) {
|
||||
let network_cancelled = deferred.cancellation_token();
|
||||
let process_exited = process.cancellation_token();
|
||||
tokio::spawn(async move {
|
||||
let denied = tokio::select! {
|
||||
_ = network_cancelled.cancelled() => true,
|
||||
_ = process_exited.cancelled() => {
|
||||
wait_for_late_network_denial(Some(network_cancelled.clone())).await
|
||||
}
|
||||
};
|
||||
if !denied {
|
||||
return;
|
||||
}
|
||||
let session = session.upgrade();
|
||||
let message = network_denial_message_for_session(session.as_ref(), Some(deferred)).await;
|
||||
process.fail_and_terminate(message);
|
||||
});
|
||||
}
|
||||
|
||||
impl UnifiedExecProcessManager {
|
||||
pub(crate) async fn allocate_process_id(&self) -> i32 {
|
||||
loop {
|
||||
@@ -212,19 +362,7 @@ impl UnifiedExecProcessManager {
|
||||
store.remove(process_id)
|
||||
};
|
||||
if let Some(entry) = removed {
|
||||
Self::unregister_network_approval_for_entry(&entry).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn unregister_network_approval_for_entry(entry: &ProcessEntry) {
|
||||
if let Some(network_approval_id) = entry.network_approval_id.as_deref()
|
||||
&& let Some(session) = entry.session.upgrade()
|
||||
{
|
||||
session
|
||||
.services
|
||||
.network_approval
|
||||
.unregister_call(network_approval_id)
|
||||
.await;
|
||||
unregister_network_approval_for_entry(&entry).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +388,13 @@ impl UnifiedExecProcessManager {
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
if let Some(deferred) = deferred_network_approval.as_ref() {
|
||||
terminate_process_on_network_denial(
|
||||
Arc::clone(&process),
|
||||
Arc::downgrade(&context.session),
|
||||
deferred.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
let event_ctx = ToolEventCtx::new(
|
||||
@@ -272,9 +417,6 @@ impl UnifiedExecProcessManager {
|
||||
// turn cannot drop the last Arc and terminate the background process.
|
||||
let process_started_alive = !process.has_exited() && process.exit_code().is_none();
|
||||
if process_started_alive {
|
||||
let network_approval_id = deferred_network_approval
|
||||
.as_ref()
|
||||
.map(|deferred| deferred.registration_id().to_string());
|
||||
self.store_process(
|
||||
Arc::clone(&process),
|
||||
context,
|
||||
@@ -284,7 +426,7 @@ impl UnifiedExecProcessManager {
|
||||
start,
|
||||
request.process_id,
|
||||
request.tty,
|
||||
network_approval_id,
|
||||
deferred_network_approval.clone(),
|
||||
Arc::clone(&transcript),
|
||||
)
|
||||
.await;
|
||||
@@ -320,27 +462,50 @@ impl UnifiedExecProcessManager {
|
||||
|
||||
let text = String::from_utf8_lossy(&collected).to_string();
|
||||
let chunk_id = generate_chunk_id();
|
||||
if let Some(message) = process.failure_message() {
|
||||
if !process_started_alive {
|
||||
emit_failed_exec_end_for_unified_exec(
|
||||
Arc::clone(&context.session),
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
cwd.clone(),
|
||||
Some(request.process_id.to_string()),
|
||||
Arc::clone(&transcript),
|
||||
message.clone(),
|
||||
wall_time,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
self.release_process_id(request.process_id).await;
|
||||
finish_deferred_network_approval(
|
||||
context.session.as_ref(),
|
||||
if deferred_network_approval
|
||||
.as_ref()
|
||||
.is_some_and(DeferredNetworkApproval::is_cancelled)
|
||||
{
|
||||
let message = network_denial_message_for_session(
|
||||
Some(&context.session),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
emit_failed_initial_exec_end_if_unstored(
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
wall_time,
|
||||
)
|
||||
.await;
|
||||
self.release_process_id(request.process_id).await;
|
||||
return Err(fail_process_with_message(process.as_ref(), message));
|
||||
}
|
||||
if let Some(message) = process.failure_message() {
|
||||
let finish_result = finish_deferred_network_approval_for_session(
|
||||
Some(&context.session),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
emit_failed_initial_exec_end_if_unstored(
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
wall_time,
|
||||
)
|
||||
.await;
|
||||
self.release_process_id(request.process_id).await;
|
||||
if let Err(message) = finish_result {
|
||||
return Err(fail_process_with_message(process.as_ref(), message));
|
||||
}
|
||||
return Err(UnifiedExecError::process_failed(message));
|
||||
}
|
||||
let process_id = request.process_id;
|
||||
@@ -351,7 +516,16 @@ impl UnifiedExecProcessManager {
|
||||
process_id,
|
||||
..
|
||||
} => (Some(process_id), exit_code),
|
||||
ProcessStatus::Exited { exit_code, .. } => {
|
||||
ProcessStatus::Exited { exit_code, entry } => {
|
||||
if let Err(message) =
|
||||
finish_deferred_network_approval_after_process_exit_for_session(
|
||||
Some(&context.session),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
return Err(fail_process_with_message(entry.process.as_ref(), message));
|
||||
}
|
||||
process.check_for_sandbox_denial_with_text(&text).await?;
|
||||
(None, exit_code)
|
||||
}
|
||||
@@ -363,6 +537,26 @@ impl UnifiedExecProcessManager {
|
||||
// Short‑lived command: emit ExecCommandEnd immediately using the
|
||||
// same helper as the background watcher, so all end events share
|
||||
// one implementation.
|
||||
let finish_result = finish_deferred_network_approval_after_process_exit_for_session(
|
||||
Some(&context.session),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
if let Err(message) = finish_result {
|
||||
emit_failed_initial_exec_end_if_unstored(
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
wall_time,
|
||||
)
|
||||
.await;
|
||||
self.release_process_id(request.process_id).await;
|
||||
return Err(fail_process_with_message(process.as_ref(), message));
|
||||
}
|
||||
let exit_code = process.exit_code();
|
||||
let exit = exit_code.unwrap_or(-1);
|
||||
emit_exec_end_for_unified_exec(
|
||||
@@ -380,11 +574,6 @@ impl UnifiedExecProcessManager {
|
||||
.await;
|
||||
|
||||
self.release_process_id(request.process_id).await;
|
||||
finish_deferred_network_approval(
|
||||
context.session.as_ref(),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
process.check_for_sandbox_denial_with_text(&text).await?;
|
||||
(None, exit_code)
|
||||
};
|
||||
@@ -419,6 +608,8 @@ impl UnifiedExecProcessManager {
|
||||
output_closed_notify,
|
||||
cancellation_token,
|
||||
pause_state,
|
||||
session,
|
||||
network_approval,
|
||||
hook_command,
|
||||
process_id,
|
||||
tty,
|
||||
@@ -478,8 +669,26 @@ impl UnifiedExecProcessManager {
|
||||
let text = String::from_utf8_lossy(&collected).to_string();
|
||||
let original_token_count = approx_token_count(&text);
|
||||
let chunk_id = generate_chunk_id();
|
||||
if let Some(message) = process.failure_message() {
|
||||
if network_approval
|
||||
.as_ref()
|
||||
.is_some_and(DeferredNetworkApproval::is_cancelled)
|
||||
{
|
||||
let message =
|
||||
network_denial_message_for_session(session.as_ref(), network_approval.clone())
|
||||
.await;
|
||||
self.release_process_id(process_id).await;
|
||||
return Err(fail_process_with_message(process.as_ref(), message));
|
||||
}
|
||||
if let Some(message) = process.failure_message() {
|
||||
let finish_result = finish_deferred_network_approval_for_session(
|
||||
session.as_ref(),
|
||||
network_approval.clone(),
|
||||
)
|
||||
.await;
|
||||
self.release_process_id(process_id).await;
|
||||
if let Err(message) = finish_result {
|
||||
return Err(fail_process_with_message(process.as_ref(), message));
|
||||
}
|
||||
return Err(UnifiedExecError::process_failed(message));
|
||||
}
|
||||
|
||||
@@ -500,6 +709,11 @@ impl UnifiedExecProcessManager {
|
||||
} => (Some(process_id), exit_code, call_id),
|
||||
ProcessStatus::Exited { exit_code, entry } => {
|
||||
let call_id = entry.call_id.clone();
|
||||
if let Err(message) =
|
||||
finish_network_approval_after_process_exit_for_entry(&entry).await
|
||||
{
|
||||
return Err(fail_process_with_message(entry.process.as_ref(), message));
|
||||
}
|
||||
(None, exit_code, call_id)
|
||||
}
|
||||
ProcessStatus::Unknown => {
|
||||
@@ -525,7 +739,7 @@ impl UnifiedExecProcessManager {
|
||||
}
|
||||
|
||||
async fn refresh_process_state(&self, process_id: i32) -> ProcessStatus {
|
||||
let status = {
|
||||
{
|
||||
let mut store = self.process_store.lock().await;
|
||||
let Some(entry) = store.processes.get(&process_id) else {
|
||||
return ProcessStatus::Unknown;
|
||||
@@ -549,11 +763,7 @@ impl UnifiedExecProcessManager {
|
||||
process_id,
|
||||
}
|
||||
}
|
||||
};
|
||||
if let ProcessStatus::Exited { entry, .. } = &status {
|
||||
Self::unregister_network_approval_for_entry(entry).await;
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
async fn prepare_process_handles(
|
||||
@@ -577,6 +787,7 @@ impl UnifiedExecProcessManager {
|
||||
.session
|
||||
.upgrade()
|
||||
.map(|session| session.subscribe_out_of_band_elicitation_pause_state());
|
||||
let session = entry.session.upgrade();
|
||||
|
||||
Ok(PreparedProcessHandles {
|
||||
process: Arc::clone(&entry.process),
|
||||
@@ -586,6 +797,8 @@ impl UnifiedExecProcessManager {
|
||||
output_closed_notify,
|
||||
cancellation_token,
|
||||
pause_state,
|
||||
session,
|
||||
network_approval: entry.network_approval.clone(),
|
||||
hook_command: entry.hook_command.clone(),
|
||||
process_id: entry.process_id,
|
||||
tty: entry.tty,
|
||||
@@ -603,7 +816,7 @@ impl UnifiedExecProcessManager {
|
||||
started_at: Instant,
|
||||
process_id: i32,
|
||||
tty: bool,
|
||||
network_approval_id: Option<String>,
|
||||
network_approval: Option<DeferredNetworkApproval>,
|
||||
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
|
||||
) {
|
||||
let entry = ProcessEntry {
|
||||
@@ -612,7 +825,7 @@ impl UnifiedExecProcessManager {
|
||||
process_id,
|
||||
hook_command,
|
||||
tty,
|
||||
network_approval_id,
|
||||
network_approval,
|
||||
session: Arc::downgrade(&context.session),
|
||||
last_used: started_at,
|
||||
};
|
||||
@@ -625,7 +838,7 @@ impl UnifiedExecProcessManager {
|
||||
// prune_processes_if_needed runs while holding process_store; do async
|
||||
// network-approval cleanup only after dropping that lock.
|
||||
if let Some(pruned_entry) = pruned_entry {
|
||||
Self::unregister_network_approval_for_entry(&pruned_entry).await;
|
||||
unregister_network_approval_for_entry(&pruned_entry).await;
|
||||
pruned_entry.process.terminate();
|
||||
}
|
||||
|
||||
@@ -1041,7 +1254,7 @@ impl UnifiedExecProcessManager {
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
Self::unregister_network_approval_for_entry(&entry).await;
|
||||
unregister_network_approval_for_entry(&entry).await;
|
||||
entry.process.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,94 @@ fn exec_server_process_id_matches_unified_exec_process_id() {
|
||||
assert_eq!(exec_server_process_id(/*process_id*/ 4321), "4321");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_denial_fallback_message_names_sandbox_network_proxy() {
|
||||
let message = network_denial_message_for_session(/*session*/ None, /*deferred*/ None).await;
|
||||
|
||||
assert_eq!(
|
||||
message,
|
||||
"Network access was denied by the Codex sandbox network proxy."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn late_network_denial_grace_observes_cancellation_after_exit() {
|
||||
let cancellation = CancellationToken::new();
|
||||
let cancellation_for_task = cancellation.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
cancellation_for_task.cancel();
|
||||
});
|
||||
|
||||
assert!(wait_for_late_network_denial(Some(cancellation)).await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
|
||||
let (session, turn, rx_event) = crate::session::tests::make_session_and_context_with_rx().await;
|
||||
let context = UnifiedExecContext::new(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
"call-unified-denied".to_string(),
|
||||
);
|
||||
let request = ExecCommandRequest {
|
||||
command: vec![
|
||||
"sh".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo before".to_string(),
|
||||
],
|
||||
hook_command: "echo before".to_string(),
|
||||
process_id: 123,
|
||||
yield_time_ms: 1000,
|
||||
max_output_tokens: None,
|
||||
workdir: None,
|
||||
network: None,
|
||||
tty: true,
|
||||
sandbox_permissions: crate::sandboxing::SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
additional_permissions_preapproved: false,
|
||||
justification: None,
|
||||
prefix_rule: None,
|
||||
};
|
||||
|
||||
let transcript = Arc::new(tokio::sync::Mutex::new(HeadTailBuffer::default()));
|
||||
transcript
|
||||
.lock()
|
||||
.await
|
||||
.push_chunk(b"PARTIAL_TRANSCRIPT".to_vec());
|
||||
|
||||
emit_failed_initial_exec_end_if_unstored(
|
||||
/*process_started_alive*/ false,
|
||||
&context,
|
||||
&request,
|
||||
turn.cwd.clone(),
|
||||
transcript,
|
||||
"PRE_DENIAL_MARKER".to_string(),
|
||||
"Network access denied".to_string(),
|
||||
Duration::from_millis(7),
|
||||
)
|
||||
.await;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), rx_event.recv())
|
||||
.await
|
||||
.expect("timed out waiting for failed exec end event")
|
||||
.expect("event channel closed");
|
||||
let codex_protocol::protocol::EventMsg::ExecCommandEnd(end_event) = event.msg else {
|
||||
panic!("expected ExecCommandEnd event");
|
||||
};
|
||||
assert_eq!(end_event.call_id, "call-unified-denied");
|
||||
assert_eq!(
|
||||
end_event.status,
|
||||
codex_protocol::protocol::ExecCommandStatus::Failed
|
||||
);
|
||||
assert_eq!(end_event.exit_code, -1);
|
||||
assert_eq!(end_event.process_id.as_deref(), Some("123"));
|
||||
assert_eq!(
|
||||
end_event.aggregated_output,
|
||||
"PRE_DENIAL_MARKER\nNetwork access denied"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_prefers_exited_processes_outside_recently_used() {
|
||||
let now = Instant::now();
|
||||
|
||||
@@ -112,6 +112,20 @@ async fn remote_write_closed_stdin_marks_process_exited() {
|
||||
assert!(process.has_exited());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_and_terminate_preserves_failure_message() {
|
||||
let process = remote_process(WriteStatus::Accepted).await;
|
||||
|
||||
process.fail_and_terminate("network denied".to_string());
|
||||
process.fail_and_terminate("second failure".to_string());
|
||||
|
||||
assert!(process.has_exited());
|
||||
assert_eq!(
|
||||
process.failure_message(),
|
||||
Some("network denied".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_process_waits_for_early_exit_event() {
|
||||
let (wake_tx, _wake_rx) = watch::channel(0);
|
||||
|
||||
Reference in New Issue
Block a user