mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Refactor network approvals to host/protocol/port scope (#12140)
## Summary Simplify network approvals by removing per-attempt proxy correlation and moving to session-level approval dedupe keyed by (host, protocol, port). Instead of encoding attempt IDs into proxy credentials/URLs, we now treat approvals as a destination policy decision. - Concurrent calls to the same destination share one approval prompt. - Different destinations (or same host on different ports) get separate prompts. - Allow once approves the current queued request group only. - Allow for session caches that (host, protocol, port) and auto-allows future matching requests. - Never policy continues to deny without prompting. Example: - 3 calls: - a.com (line 443) - b.com (line 443) - a.com (line 443) => 2 prompts total (a, b), second a waits on the first decision. - a.com:80 is treated separately from a.com line 443 ## Testing - `just fmt` (in `codex-rs`) - `cargo test -p codex-core tools::network_approval::tests` - `cargo test -p codex-core` (unit tests pass; existing integration-suite failures remain in this environment)
This commit is contained in:
committed by
GitHub
Unverified
parent
41f15bf07b
commit
e8afaed502
@@ -139,43 +139,6 @@ pub(crate) fn spawn_exit_watcher(
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_network_denial_watcher(
|
||||
process: Arc<UnifiedExecProcess>,
|
||||
session: Arc<Session>,
|
||||
process_id: String,
|
||||
network_attempt_id: String,
|
||||
) {
|
||||
let exit_token = process.cancellation_token();
|
||||
tokio::spawn(async move {
|
||||
let mut poll = tokio::time::interval(Duration::from_millis(100));
|
||||
poll.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = exit_token.cancelled() => {
|
||||
break;
|
||||
}
|
||||
_ = poll.tick() => {
|
||||
if session
|
||||
.services
|
||||
.network_approval
|
||||
.take_user_denial_outcome(&network_attempt_id)
|
||||
.await
|
||||
{
|
||||
process.terminate();
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.release_process_id(&process_id)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn process_chunk(
|
||||
pending: &mut Vec<u8>,
|
||||
transcript: &Arc<Mutex<HeadTailBuffer>>,
|
||||
|
||||
@@ -155,7 +155,7 @@ struct ProcessEntry {
|
||||
process_id: String,
|
||||
command: Vec<String>,
|
||||
tty: bool,
|
||||
network_attempt_id: Option<String>,
|
||||
network_approval_id: Option<String>,
|
||||
session: Weak<Session>,
|
||||
last_used: tokio::time::Instant,
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ use crate::tools::events::ToolEmitter;
|
||||
use crate::tools::events::ToolEventCtx;
|
||||
use crate::tools::events::ToolEventStage;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
use crate::tools::network_approval::deferred_rejection_message;
|
||||
use crate::tools::network_approval::finish_deferred_network_approval;
|
||||
use crate::tools::orchestrator::ToolOrchestrator;
|
||||
use crate::tools::runtimes::unified_exec::UnifiedExecRequest as UnifiedExecToolRequest;
|
||||
@@ -44,7 +43,6 @@ use crate::unified_exec::WARNING_UNIFIED_EXEC_PROCESSES;
|
||||
use crate::unified_exec::WriteStdinRequest;
|
||||
use crate::unified_exec::async_watcher::emit_exec_end_for_unified_exec;
|
||||
use crate::unified_exec::async_watcher::spawn_exit_watcher;
|
||||
use crate::unified_exec::async_watcher::spawn_network_denial_watcher;
|
||||
use crate::unified_exec::async_watcher::start_streaming_output;
|
||||
use crate::unified_exec::clamp_yield_time;
|
||||
use crate::unified_exec::generate_chunk_id;
|
||||
@@ -140,18 +138,18 @@ impl UnifiedExecProcessManager {
|
||||
store.remove(process_id)
|
||||
};
|
||||
if let Some(entry) = removed {
|
||||
Self::unregister_network_attempt_for_entry(&entry).await;
|
||||
Self::unregister_network_approval_for_entry(&entry).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn unregister_network_attempt_for_entry(entry: &ProcessEntry) {
|
||||
if let Some(attempt_id) = entry.network_attempt_id.as_deref()
|
||||
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_attempt(attempt_id)
|
||||
.unregister_call(network_approval_id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
@@ -248,17 +246,6 @@ impl UnifiedExecProcessManager {
|
||||
.await;
|
||||
|
||||
self.release_process_id(&request.process_id).await;
|
||||
if let Some(deferred) = deferred_network_approval.as_ref()
|
||||
&& let Some(message) =
|
||||
deferred_rejection_message(context.session.as_ref(), deferred).await
|
||||
{
|
||||
finish_deferred_network_approval(
|
||||
context.session.as_ref(),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
return Err(UnifiedExecError::create_process(message));
|
||||
}
|
||||
finish_deferred_network_approval(
|
||||
context.session.as_ref(),
|
||||
deferred_network_approval.take(),
|
||||
@@ -266,27 +253,13 @@ impl UnifiedExecProcessManager {
|
||||
.await;
|
||||
process.check_for_sandbox_denial_with_text(&text).await?;
|
||||
} else {
|
||||
if let Some(deferred) = deferred_network_approval.as_ref()
|
||||
&& let Some(message) =
|
||||
deferred_rejection_message(context.session.as_ref(), deferred).await
|
||||
{
|
||||
process.terminate();
|
||||
finish_deferred_network_approval(
|
||||
context.session.as_ref(),
|
||||
deferred_network_approval.take(),
|
||||
)
|
||||
.await;
|
||||
self.release_process_id(&request.process_id).await;
|
||||
return Err(UnifiedExecError::create_process(message));
|
||||
}
|
||||
|
||||
// Long‑lived command: persist the process so write_stdin can reuse
|
||||
// it, and register a background watcher that will emit
|
||||
// ExecCommandEnd when the PTY eventually exits (even if no further
|
||||
// tool calls are made).
|
||||
let network_attempt_id = deferred_network_approval
|
||||
let network_approval_id = deferred_network_approval
|
||||
.as_ref()
|
||||
.map(|deferred| deferred.attempt_id().to_string());
|
||||
.map(|deferred| deferred.registration_id().to_string());
|
||||
self.store_process(
|
||||
Arc::clone(&process),
|
||||
context,
|
||||
@@ -295,7 +268,7 @@ impl UnifiedExecProcessManager {
|
||||
start,
|
||||
process_id,
|
||||
request.tty,
|
||||
network_attempt_id,
|
||||
network_approval_id,
|
||||
Arc::clone(&transcript),
|
||||
)
|
||||
.await;
|
||||
@@ -443,7 +416,7 @@ impl UnifiedExecProcessManager {
|
||||
}
|
||||
};
|
||||
if let ProcessStatus::Exited { entry, .. } = &status {
|
||||
Self::unregister_network_attempt_for_entry(entry).await;
|
||||
Self::unregister_network_approval_for_entry(entry).await;
|
||||
}
|
||||
status
|
||||
}
|
||||
@@ -502,17 +475,16 @@ impl UnifiedExecProcessManager {
|
||||
started_at: Instant,
|
||||
process_id: String,
|
||||
tty: bool,
|
||||
network_attempt_id: Option<String>,
|
||||
network_approval_id: Option<String>,
|
||||
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
|
||||
) {
|
||||
let network_attempt_id_for_watcher = network_attempt_id.clone();
|
||||
let entry = ProcessEntry {
|
||||
process: Arc::clone(&process),
|
||||
call_id: context.call_id.clone(),
|
||||
process_id: process_id.clone(),
|
||||
command: command.to_vec(),
|
||||
tty,
|
||||
network_attempt_id,
|
||||
network_approval_id,
|
||||
session: Arc::downgrade(&context.session),
|
||||
last_used: started_at,
|
||||
};
|
||||
@@ -525,7 +497,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_attempt_for_entry(&pruned_entry).await;
|
||||
Self::unregister_network_approval_for_entry(&pruned_entry).await;
|
||||
pruned_entry.process.terminate();
|
||||
}
|
||||
|
||||
@@ -550,17 +522,6 @@ impl UnifiedExecProcessManager {
|
||||
transcript,
|
||||
started_at,
|
||||
);
|
||||
|
||||
if context.turn.config.managed_network_requirements_enabled()
|
||||
&& let Some(network_attempt_id) = network_attempt_id_for_watcher
|
||||
{
|
||||
spawn_network_denial_watcher(
|
||||
Arc::clone(&process),
|
||||
Arc::clone(&context.session),
|
||||
process_id,
|
||||
network_attempt_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn open_session_with_exec_env(
|
||||
@@ -637,7 +598,6 @@ impl UnifiedExecProcessManager {
|
||||
turn: context.turn.as_ref(),
|
||||
call_id: context.call_id.clone(),
|
||||
tool_name: "exec_command".to_string(),
|
||||
network_attempt_id: None,
|
||||
};
|
||||
orchestrator
|
||||
.run(
|
||||
@@ -792,7 +752,7 @@ impl UnifiedExecProcessManager {
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
Self::unregister_network_attempt_for_entry(&entry).await;
|
||||
Self::unregister_network_approval_for_entry(&entry).await;
|
||||
entry.process.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user