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

@@ -122,7 +122,7 @@ impl ApprovalOverlay {
|| "Would you like to run the following command?".to_string(),
|network_approval_context| {
format!(
"Do you want to approve access to \"{}\"?",
"Do you want to approve network access to \"{}\"?",
network_approval_context.host
)
},
@@ -364,12 +364,14 @@ impl From<ApprovalRequest> for ApprovalRequestState {
header.push(Line::from(vec!["Reason: ".into(), reason.italic()]));
header.push(Line::from(""));
}
let full_cmd = strip_bash_lc_and_escape(&command);
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
if let Some(first) = full_cmd_lines.first_mut() {
first.spans.insert(0, Span::from("$ "));
if network_approval_context.is_none() {
let full_cmd = strip_bash_lc_and_escape(&command);
let mut full_cmd_lines = highlight_bash_to_lines(&full_cmd);
if let Some(first) = full_cmd_lines.first_mut() {
first.spans.insert(0, Span::from("$ "));
}
header.extend(full_cmd_lines);
}
header.extend(full_cmd_lines);
Self {
variant: ApprovalVariant::Exec {
id,
@@ -738,11 +740,15 @@ mod tests {
.collect();
assert!(
rendered
.iter()
.any(|line| line.contains("Do you want to approve access to \"example.com\"?")),
rendered.iter().any(|line| {
line.contains("Do you want to approve network access to \"example.com\"?")
}),
"expected network title to include host, got {rendered:?}"
);
assert!(
!rendered.iter().any(|line| line.contains("$ curl")),
"network prompt should not show command line, got {rendered:?}"
);
assert!(
!rendered.iter().any(|line| line.contains("don't ask again")),
"network prompt should not show execpolicy option, got {rendered:?}"