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:
viyatb-oai
2026-02-20 10:39:55 -08:00
committed by GitHub
parent 41f15bf07b
commit e8afaed502
40 changed files with 570 additions and 739 deletions

View File

@@ -44,6 +44,7 @@ use codex_app_server_protocol::McpToolCallError;
use codex_app_server_protocol::McpToolCallResult;
use codex_app_server_protocol::McpToolCallStatus;
use codex_app_server_protocol::ModelReroutedNotification;
use codex_app_server_protocol::NetworkApprovalContext as V2NetworkApprovalContext;
use codex_app_server_protocol::PatchApplyStatus;
use codex_app_server_protocol::PlanDeltaNotification;
use codex_app_server_protocol::RawResponseItemCompletedNotification;
@@ -106,6 +107,17 @@ use tracing::error;
type JsonValue = serde_json::Value;
enum CommandExecutionApprovalPresentation {
Network(V2NetworkApprovalContext),
Command(CommandExecutionCompletionItem),
}
struct CommandExecutionCompletionItem {
command: String,
cwd: PathBuf,
command_actions: Vec<V2ParsedCommand>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn apply_bespoke_event_handling(
event: Event,
@@ -245,6 +257,7 @@ pub(crate) async fn apply_bespoke_event_handling(
command,
cwd,
reason,
network_approval_context,
proposed_execpolicy_amendment,
parsed_cmd,
..
@@ -280,7 +293,32 @@ pub(crate) async fn apply_bespoke_event_handling(
.cloned()
.map(V2ParsedCommand::from)
.collect::<Vec<_>>();
let command_string = shlex_join(&command);
let presentation = if let Some(network_approval_context) =
network_approval_context.map(V2NetworkApprovalContext::from)
{
CommandExecutionApprovalPresentation::Network(network_approval_context)
} else {
let command_string = shlex_join(&command);
let completion_item = CommandExecutionCompletionItem {
command: command_string,
cwd: cwd.clone(),
command_actions: command_actions.clone(),
};
CommandExecutionApprovalPresentation::Command(completion_item)
};
let (network_approval_context, command, cwd, command_actions, completion_item) =
match presentation {
CommandExecutionApprovalPresentation::Network(
network_approval_context,
) => (Some(network_approval_context), None, None, None, None),
CommandExecutionApprovalPresentation::Command(completion_item) => (
None,
Some(completion_item.command.clone()),
Some(completion_item.cwd.clone()),
Some(completion_item.command_actions.clone()),
Some(completion_item),
),
};
let proposed_execpolicy_amendment_v2 =
proposed_execpolicy_amendment.map(V2ExecPolicyAmendment::from);
@@ -290,9 +328,10 @@ pub(crate) async fn apply_bespoke_event_handling(
item_id: call_id.clone(),
approval_id: approval_id.clone(),
reason,
command: Some(command_string.clone()),
cwd: Some(cwd.clone()),
command_actions: Some(command_actions.clone()),
network_approval_context,
command,
cwd,
command_actions,
proposed_execpolicy_amendment: proposed_execpolicy_amendment_v2,
};
let rx = outgoing
@@ -306,9 +345,7 @@ pub(crate) async fn apply_bespoke_event_handling(
conversation_id,
approval_id,
call_id,
command_string,
cwd,
command_actions,
completion_item,
rx,
conversation,
outgoing,
@@ -1790,9 +1827,7 @@ async fn on_command_execution_request_approval_response(
conversation_id: ThreadId,
approval_id: Option<String>,
item_id: String,
command: String,
cwd: PathBuf,
command_actions: Vec<V2ParsedCommand>,
completion_item: Option<CommandExecutionCompletionItem>,
receiver: oneshot::Receiver<ClientRequestResult>,
conversation: Arc<CodexThread>,
outgoing: ThreadScopedOutgoingMessageSender,
@@ -1864,15 +1899,16 @@ async fn on_command_execution_request_approval_response(
if let Some(status) = completion_status
&& !suppress_subcommand_completion_item
&& let Some(completion_item) = completion_item
{
complete_command_execution_item(
conversation_id,
event_turn_id.clone(),
item_id.clone(),
command.clone(),
cwd.clone(),
completion_item.command,
completion_item.cwd,
None,
command_actions.clone(),
completion_item.command_actions,
status,
&outgoing,
)