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
@@ -110,6 +110,30 @@
"type": "object"
}
]
},
"NetworkApprovalContext": {
"properties": {
"host": {
"type": "string"
},
"protocol": {
"$ref": "#/definitions/NetworkApprovalProtocol"
}
},
"required": [
"host",
"protocol"
],
"type": "object"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
"https",
"socks5Tcp",
"socks5Udp"
],
"type": "string"
}
},
"properties": {
@@ -147,6 +171,17 @@
"itemId": {
"type": "string"
},
"networkApprovalContext": {
"anyOf": [
{
"$ref": "#/definitions/NetworkApprovalContext"
},
{
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
"items": {
@@ -213,6 +213,17 @@
"itemId": {
"type": "string"
},
"networkApprovalContext": {
"anyOf": [
{
"$ref": "#/definitions/NetworkApprovalContext"
},
{
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
"items": {
@@ -419,6 +430,30 @@
],
"type": "object"
},
"NetworkApprovalContext": {
"properties": {
"host": {
"type": "string"
},
"protocol": {
"$ref": "#/definitions/NetworkApprovalProtocol"
}
},
"required": [
"host",
"protocol"
],
"type": "object"
},
"NetworkApprovalProtocol": {
"enum": [
"http",
"https",
"socks5Tcp",
"socks5Udp"
],
"type": "string"
},
"ParsedCommand": {
"oneOf": [
{
@@ -2192,6 +2192,17 @@
"itemId": {
"type": "string"
},
"networkApprovalContext": {
"anyOf": [
{
"$ref": "#/definitions/NetworkApprovalContext"
},
{
"type": "null"
}
],
"description": "Optional context for managed-network approval prompts."
},
"proposedExecpolicyAmendment": {
"description": "Optional proposed execpolicy amendment to allow similar commands without prompting.",
"items": {
@@ -3,6 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CommandAction } from "./CommandAction";
import type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
import type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
/**
@@ -19,6 +20,10 @@ approvalId?: string | null,
* Optional explanatory reason (e.g. request for network access).
*/
reason?: string | null,
/**
* Optional context for managed-network approval prompts.
*/
networkApprovalContext?: NetworkApprovalContext | null,
/**
* The command to be executed.
*/
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type NetworkApprovalContext = { host: string, protocol: NetworkApprovalProtocol, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NetworkApprovalProtocol = "http" | "https" | "socks5Tcp" | "socks5Udp";
@@ -100,6 +100,8 @@ export type { ModelListResponse } from "./ModelListResponse";
export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { NetworkAccess } from "./NetworkAccess";
export type { NetworkApprovalContext } from "./NetworkApprovalContext";
export type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
export type { NetworkRequirements } from "./NetworkRequirements";
export type { OverriddenMetadata } from "./OverriddenMetadata";
export type { PatchApplyStatus } from "./PatchApplyStatus";
@@ -5,6 +5,8 @@ use crate::protocol::common::AuthMode;
use codex_experimental_api_macros::ExperimentalApi;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext;
use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::CollaborationModeMask;
use codex_protocol::config_types::ForcedLoginMethod;
@@ -650,6 +652,32 @@ pub enum CommandExecutionApprovalDecision {
Cancel,
}
v2_enum_from_core! {
pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol {
Http,
Https,
Socks5Tcp,
Socks5Udp,
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct NetworkApprovalContext {
pub host: String,
pub protocol: NetworkApprovalProtocol,
}
impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
fn from(value: CoreNetworkApprovalContext) -> Self {
Self {
host: value.host,
protocol: value.protocol.into(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -3331,6 +3359,10 @@ pub struct CommandExecutionRequestApprovalParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub reason: Option<String>,
/// Optional context for managed-network approval prompts.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub network_approval_context: Option<NetworkApprovalContext>,
/// The command to be executed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]