Skip to content

网络审批与规则保存

追踪网络阻断归因、host 审批并发、Deferred outcome、会话缓存和 network policy 规则保存。

基于rust-v0.150.0
CodexRustToolsNetwork

网络审批与规则保存 ​

网络审批发生在请求已经抵达网络代理之后。代理先产生 BlockedRequest,Codex 再把它归因到某个活动工具调用,按 environment_id + host + protocol + port 查找会话状态;如果没有现成决定,同一个 host 的并发请求共享一个 pending approval。用户或 Guardian 的决定随后可能只放行本次、放行本 session,或把 allow/deny 写入 network policy 文件。

本文面向已经读过工具审批架构、ToolOrchestrator执行流程 和ToolOutput与错误模型的读者。前文解释通用 reviewer 路由、sandbox retry 和工具错误,本文只研究网络策略 在代理阻断后的专用状态机,不重复一般 command approval,也不展开代理实现和 MCP elicitation。读完后,读者应能解释一个 网络阻断如何找到所属 call、为什么 HTTP 和 SOCKS5 不共享批准、Deferred 如何改变失败时机,以及规则保存失败 时为什么不会误把 host 当作已授权。

1. 网络边界 ​

1.1 代理阻断 ​

工具 runtime 只有在 managed network active 时才创建网络审批 spec。spec 携带执行代理、触发信息、 command、environment 和 permission profile;真正的网络请求由代理在运行时观察并回调 NetworkPolicyDecider。

相关源码:

  • codex-rs/core/src/tools/sandboxing.rs :: ToolRuntime::network_approval_spec
  • codex-rs/core/src/tools/runtimes/unified_exec.rs :: UnifiedExecRuntime::network_approval_spec
rust
let managed_network_active = turn_ctx.network.is_some();
let network_approval = begin_network_approval(
    &tool_ctx.session,
    turn_ctx,
    managed_network_active,
    tool.network_approval_spec(req, tool_ctx),
)
.await?;

当前实现不再由 runtime 暴露独立的两阶段枚举;ToolOrchestrator::run_attempt 在工具结果返回后, 把活动审批转换为 DeferredNetworkApproval,统一交给后续 finish 路径。Unified Exec 进程 manager 还会在进程退出或晚到 网络拒绝时再次 finish,保证网络 outcome 不因工具先返回而丢失。

1.2 四元键 ​

网络审批不会只按域名缓存。HostApprovalKey 将 host 转成 ASCII 小写,并保留 environment、协议标签和端口;因此 HTTP example.com:80、HTTPS example.com:443、SOCKS5 example.com:1080 是三个不同的决策对象。

源码位置:codex-rs/core/src/tools/network_approval.rs :: HostApprovalKey::from_request

rust
fn from_request(
    request: &NetworkPolicyRequest,
    protocol: NetworkApprovalProtocol,
    environment_id: String,
) -> Self {
    Self {
        environment_id,
        host: request.host.to_ascii_lowercase(),
        protocol: protocol_key_label(protocol),
        port: request.port,
    }
}

2. 活动调用 ​

2.1 注册代理 ​

begin_network_approval 为一次工具执行生成 registration id 和 attribution token,创建 execution-scoped proxy,并把 ActiveNetworkApprovalCall 放入 active_calls。取消令牌同时交给代理和后续 pending owner;网络拒绝时可以让正在运行的 命令尽快停止。

源码位置:codex-rs/core/src/tools/network_approval.rs :: begin_network_approval

rust
let registration_id = Uuid::new_v4().to_string();
let attribution_token = Uuid::new_v4().to_string();
let execution_proxy = network
    .for_execution(
        &environment_id,
        &registration_id,
        attribution_token,
        environment_policy,
        fallback_policy_decider,
    )
    .map_err(|err| {
        ToolError::Codex(codex_protocol::error::CodexErr::Io(
            io::Error::other(format!(
                "failed to create execution-scoped network proxy: {err}"
            )),
        ))
    })?;
let cancellation_token = CancellationToken::new();
session
    .services
    .network_approval
    .register_call(ActiveNetworkApprovalCall {
        registration_id: registration_id.clone(),
        turn_id: turn.sub_id.clone(),
        trigger,
        tool_name,
        command,
        environment_id,
        permission_profile,
        cancellation_token: cancellation_token.clone(),
    })
    .await;

2.2 请求归因 ​

代理回调可能携带 execution_id,也可能只有 environment,甚至两者都没有。服务优先使用 execution id 精确查找;没有 execution id 时,只有一个活动调用才允许推断归属;多个活动调用会得到 Ambiguous,避免把一个 host 阻断错误地归给另一个 命令。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::resolve_request_attribution

rust
if let Some(execution_id) = request.execution_id.as_deref() {
    let call = self
        .resolve_active_call_by_execution_id(execution_id)
        .await?;
    let environment_id = request
        .environment_id
        .clone()
        .unwrap_or_else(|| call.environment_id.clone());
    return (call.environment_id == environment_id).then_some(
        NetworkRequestAttribution {
            owner_call: Some(call),
            environment_id: Some(environment_id),
        },
    );
}

2.3 结束清理 ​

ActiveNetworkApproval 在一次 attempt 中持有 registration、取消令牌和 execution proxy。工具返回后, ToolOrchestrator::run_attempt 将它转换成 DeferredNetworkApproval;如果工具已经失败,会立即 finish, 成功结果则把 deferred handle 交给上层。两条路径最终都会从 active_calls 移除 registration,并把记录的 outcome 转换成 ToolError。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run_attempt

rust
let network_approval = begin_network_approval(
    &tool_ctx.session,
    &tool_ctx.step_context.turn,
    attempt.enforce_managed_network,
    network_approval_spec,
)
.await?;
let run_result = tool
    .run(req, &attempt_with_network_approval, &attempt_tool_ctx)
    .await;
let deferred = network_approval.into_deferred();
if run_result.is_err() {
    let finalize_result =
        finish_deferred_network_approval(&tool_ctx.session, deferred).await;
    if let Err(err) = finalize_result {
        return (Err(err), None);
    }
    return (run_result, None);
}
(run_result, deferred)

3. 阻断处理 ​

3.1 观察者入口 ​

代理的 blocked observer 不直接弹 UI,而是先记录 sandbox violation,再把 BlockedRequest 交给 NetworkApprovalService::record_blocked_request。服务只有找到了唯一 owner 才会写入 call_outcomes 并取消该 call。

源码位置:codex-rs/core/src/tools/network_approval.rs :: build_blocked_request_observer

rust
pub(crate) fn build_blocked_request_observer(
    network_approval: Arc<NetworkApprovalService>,
) -> Arc<dyn BlockedRequestObserver> {
    Arc::new(move |blocked: BlockedRequest| {
        let network_approval = Arc::clone(&network_approval);
        async move {
            record_network_sandbox_violation(&blocked);
            network_approval.record_blocked_request(blocked).await;
        }
    })
}

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::record_blocked_request

rust
let Some(message) = denied_network_policy_message(&blocked) else {
    return;
};
let owner_call = if let Some(execution_id) = blocked.execution_id.as_deref() {
    self.resolve_active_call_by_execution_id(execution_id).await
} else {
    self.resolve_single_active_call().await
};
let Some(owner_call) = owner_call else {
    return;
};
self.record_call_outcome_if_absent(&owner_call.registration_id, message);

3.2 内联决策器 ​

build_network_policy_decider 是代理的同步概念与 Session 的异步服务之间的适配器。Session 已被 drop 时返回 NetworkDecision::ask("not_allowed");否则调用 handle_inline_policy_request,由它处理缓存、pending approval 和 reviewer。

源码位置:codex-rs/core/src/tools/network_approval.rs :: build_network_policy_decider

rust
pub(crate) fn build_network_policy_decider(
    network_approval: Arc<NetworkApprovalService>,
    network_policy_decider_session: Arc<RwLock<std::sync::Weak<Session>>>,
) -> Arc<dyn NetworkPolicyDecider> {
    Arc::new(move |request: NetworkPolicyRequest| {
        let network_approval = Arc::clone(&network_approval);
        let network_policy_decider_session = Arc::clone(&network_policy_decider_session);
        async move {
            let Some(session) = network_policy_decider_session.read().await.upgrade() else {
                return NetworkDecision::ask("not_allowed");
            };
            network_approval
                .handle_inline_policy_request(session, request)
                .await
        }
    })
}

4. Pending并发 ​

4.1 Pending键 ​

同一个 host 的并发请求不应该弹多个相同审批框,但不同 turn 或 execution 不能互相唤醒。PendingHostApprovalKey 在 HostApprovalKey 外增加 turn_id 和可选 execution id,因此服务能共享正确范围的等待者。

源码位置:codex-rs/core/src/tools/network_approval.rs :: PendingHostApprovalKey

rust
struct PendingHostApprovalKey {
    host: HostApprovalKey,
    turn_id: String,
    execution_id: Option<String>,
}

get_or_create_pending_approval 返回 (pending, is_owner)。第一个请求成为 owner,负责运行 Hook/Guardian/User;后续请求 只等待 PendingHostApproval::wait_for_decision,不会重复发起 reviewer。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::get_or_create_pending_approval

rust
let (pending, is_owner) = self.get_or_create_pending_approval(pending_key.clone());
if !is_owner {
    return pending.wait_for_decision().await.to_network_decision();
}
let mut pending_owner = PendingHostApprovalOwner::new(
    self,
    pending_key,
    Arc::clone(&pending),
    owner_call
        .as_ref()
        .map(|call| call.cancellation_token.clone()),
);

4.2 Drop清理 ​

PendingHostApprovalOwner 是 owner 的 RAII 清理边界。正常完成时先从 map 删除当前 generation,再唤醒等待者;如果 owner 在完成前 drop,则使用 decision_on_drop,默认 Deny,并取消关联执行。删除前检查 Arc::ptr_eq,避免旧 owner 删除了 同 key 的新一代 pending。

源码位置:codex-rs/core/src/tools/network_approval.rs :: PendingHostApprovalOwner::publish_and_remove

rust
fn publish_and_remove(&self, decision: PendingApprovalDecision) {
    let mut approvals = self
        .service
        .pending_host_approvals
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    if approvals
        .get(&self.key)
        .is_some_and(|current| Arc::ptr_eq(current, &self.pending))
    {
        approvals.remove(&self.key);
    }
    drop(approvals);
    self.pending.set_decision(decision);
}

4.3 决策广播 ​

PendingHostApproval 用 SyncOnceLock 保证决定只写一次,Notify 唤醒所有等待者。AllowOnce 和 AllowForSession 都映射为代理的 NetworkDecision::Allow;Deny 映射为带 not_allowed 的拒绝。

源码位置:codex-rs/core/src/tools/network_approval.rs :: PendingHostApproval

rust
async fn wait_for_decision(&self) -> PendingApprovalDecision {
    loop {
        let notified = self.notify.notified();
        if let Some(decision) = self.decision.get() {
            return *decision;
        }
        notified.await;
    }
}

fn set_decision(&self, decision: PendingApprovalDecision) {
    if self.decision.set(decision).is_ok() {
        self.notify.notify_waiters();
    }
}

5. 决策路径 ​

5.1 缓存优先 ​

handle_inline_policy_request 在创建 pending 前先检查 session_denied_hosts 和 session_approved_hosts,两者都在 session_policy_commit_lock 下读取。已 deny 的 host 立即拒绝,已 approve 的 host 立即允许,不会再访问 reviewer。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::handle_inline_policy_request

rust
let _commit_guard = self.session_policy_commit_lock.lock().await;
{
    let denied_hosts = self.session_denied_hosts.lock().await;
    if denied_hosts.contains(&key) {
        return NetworkDecision::deny(REASON_NOT_ALLOWED);
    }
}

let approved_hosts = self.session_approved_hosts.lock().await;
if approved_hosts.contains(&key) {
    return NetworkDecision::Allow;
}

5.2 Hook与Guardian ​

没有缓存命中时,owner 先运行 permission request hook。Hook Allow 直接放行;Hook Deny 写入 policy outcome 并唤醒所有等待者。 Hook 没有决定时,routes_approval_to_guardian 选择 Guardian 或用户 command approval。Guardian 请求的 target 包含协议、host、 port 和触发工具;User 请求使用 network_approval_context,因此界面可以显示“哪个 host 被阻止”。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::handle_inline_policy_request

rust
let hook_approval_decision = match run_permission_request_hooks(
    &session,
    &turn_context,
    &hook_run_id_suffix,
    PermissionRequestPayload::bash(
        command,
        Some(format!("network-access {target}")),
    ),
)
.await
{
    Some(PermissionRequestDecision::Allow) => Some(ReviewDecision::Approved),
    Some(PermissionRequestDecision::Deny { message }) => {
        if let Some(owner_call) = owner_call.as_ref() {
            self.record_call_outcome(&owner_call.registration_id, message);
        }
        pending_owner.complete(PendingApprovalDecision::Deny);
        return NetworkDecision::deny(REASON_NOT_ALLOWED);
    }
    None => None,
};

5.3 决定映射 ​

网络审批把 ReviewDecision 映射到三个不同层次:Approved 是一次性 allow;ApprovedForSession 写入 host cache; NetworkPolicyAmendment 先尝试持久化规则,再依据持久化结果决定是否更新 session cache。网络审批服务收到 ApprovedMcpPolicyAmendment、Denied、TimedOut 或 Abort 时会记录 centralized decision error 并让 pending owner Deny; 这些变体不在网络审批服务内再次解释为不同的用户拒绝来源。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::handle_inline_policy_request

rust
ReviewDecision::ApprovedForSession => {
    if self.session_denied_hosts.lock().await.contains(&key) {
        if let Some(owner_call) = owner_call.as_ref() {
            self.record_call_outcome(
                &owner_call.registration_id,
                policy_denial_message.clone(),
            );
        }
        PendingApprovalDecision::Deny
    } else {
        self.session_approved_hosts.lock().await.insert(key.clone());
        PendingApprovalDecision::AllowForSession
    }
}

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::handle_inline_policy_request

rust
ReviewDecision::Denied { rejection } => {
    if let Some(owner_call) = owner_call.as_ref() {
        self.record_call_outcome(&owner_call.registration_id, rejection);
    }
    PendingApprovalDecision::Deny
}
ReviewDecision::TimedOut => {
    if let Some(owner_call) = owner_call.as_ref() {
        self.record_call_outcome(
            &owner_call.registration_id,
            crate::guardian::guardian_timeout_message(),
        );
    }
    PendingApprovalDecision::Deny
}

6. 规则保存 ​

6.1 Allow规则 ​

选择 NetworkPolicyAmendment::Allow 时,服务调用 Session::persist_network_policy_amendment,同时传入 NetworkApprovalContext。该函数在有运行中 proxy 时先更新 proxy,再追加 execpolicy 规则;proxy 更新成功会先把 owner 的 drop decision 设为 AllowForSession。随后规则文件写入失败会发送 Warning,但当前 Session 可能已经保留这次内存 allow;只有 重新启动 Session 时,是否继续允许才取决于规则文件是否写入成功。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkPolicyRuleAction::Allow

rust
match session
    .persist_network_policy_amendment(
        &network_policy_amendment,
        &network_approval_context,
        || {
            pending_owner
                .set_decision_on_drop(PendingApprovalDecision::AllowForSession);
        },
    )
    .await
{
    Ok(()) => {
        session
            .record_network_policy_amendment_message(
                &turn_context.sub_id,
                &network_policy_amendment,
            )
            .await;
    }
    Err(err) => {
        let message = format!("Failed to apply network policy amendment: {err}");
        warn!("{message}");
        session
            .send_event_raw(Event {
                id: turn_context.sub_id.clone(),
                msg: EventMsg::Warning(WarningEvent { message }),
            })
            .await;
    }
}

6.2 Deny规则 ​

选择 NetworkPolicyAmendment::Deny 会尝试保存 deny 规则,然后移除 approved host、加入 denied host,并把当前 owner outcome 记为用户拒绝。当前实现即使规则保存失败也会更新内存 denied cache;失败只通过 Warning 反馈,后续同 key 请求仍会在当前 Session 内直接拒绝,但重新启动 Session 是否加载该规则取决于持久化是否成功。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkPolicyRuleAction::Deny

rust
if let Some(owner_call) = owner_call.as_ref() {
    self.record_call_outcome(
        &owner_call.registration_id,
        "rejected by user".to_string(),
    );
}
self.session_approved_hosts.lock().await.remove(&key);
self.session_denied_hosts.lock().await.insert(key.clone());
PendingApprovalDecision::Deny

6.3 提交锁 ​

session_policy_commit_lock 同时保护 approved/denied host cache 和规则保存相关的提交顺序。它避免一个请求在规则落盘 过程中被另一个请求看到半完成状态。sync_session_approved_hosts_to 也在同一把锁下复制 session approved hosts,适用于 子 session 继承当前已批准 host 的场景。

源码位置:codex-rs/core/src/tools/network_approval.rs :: NetworkApprovalService::sync_session_approved_hosts_to

rust
let _commit_guard = self.session_policy_commit_lock.lock().await;
let approved_hosts = self.session_approved_hosts.lock().await.clone();
let mut other_approved_hosts = other.session_approved_hosts.lock().await;
other_approved_hosts.clear();
other_approved_hosts.extend(approved_hosts.iter().cloned());

7. 延迟审批收尾 ​

7.1 收尾时机 ​

当前 ToolOrchestrator::run_attempt 对所有带 network approval spec 的 runtime 使用同一 deferred handle:工具 结果失败时立即 finish,工具成功时把 DeferredNetworkApproval 返回给上层。Unified Exec process manager 在 进程退出、晚到网络拒绝和取消路径中继续调用 finish,因此网络结果不会因为工具先返回而丢失。

源码位置:codex-rs/core/src/tools/orchestrator.rs :: ToolOrchestrator::run_attempt

rust
let run_result = tool
    .run(req, &attempt_with_network_approval, &attempt_tool_ctx)
    .await;

let Some(network_approval) = network_approval else {
    return (run_result, None);
};

let deferred = network_approval.into_deferred();
if run_result.is_err() {
    let finalize_result =
        finish_deferred_network_approval(&tool_ctx.session, deferred).await;
    if let Err(err) = finalize_result {
        return (Err(err), None);
    }
    return (run_result, None);
}
(run_result, deferred)

run_attempt 本身只负责把 Deferred handle 交给上层;unified exec process manager 在进程退出、晚到的网络拒绝或失败输出 路径中继续调用 finish_deferred_network_approval。因此 Deferred 不是“工具成功就忽略网络结果”,而是把最终检查延后到 进程生命周期更适合的时机。

7.2 取消 ​

网络拒绝会取消 active call 的 cancellation token;owner drop 也默认 Deny 并取消 execution。若审批在决定返回前被取消, abandoned_network_approval_outcome 把 cancellation 转为明确的 policy rejection,而不是无限等待。

源码位置:codex-rs/core/src/tools/network_approval.rs :: abandoned_network_approval_outcome

rust
fn abandoned_network_approval_outcome(
    cancellation_token: &CancellationToken,
) -> Option<String> {
    cancellation_token
        .is_cancelled()
        .then(|| ABANDONED_NETWORK_APPROVAL_MESSAGE.to_string())
}

8. 非正常路径 ​

8.1 无法归因 ​

没有 execution id 且存在多个 active calls 时,服务不会猜 owner;请求被拒绝或保持 ask,具体取决于代理调用点。这个保守 策略牺牲了一次自动放行,换取不会把 host allow 错绑到另一条命令。

8.2 没有活动Turn ​

如果 active turn、environment 或 permission profile 不存在,服务会记录 policy outcome 并返回 not_allowed。网络审批不 会为了填补缺失上下文而创建一个脱离 turn 的全局审批。

8.3 保存失败 ​

规则保存失败时发送 Warning。若运行中 proxy 已先更新成功,当前 Session 会保留对应的 approved/denied cache;若没有可更新的 运行中 proxy,则 cache 只有在 execpolicy 写入成功后才会更新。后续请求是否重新审批因此取决于当前 Session 的内存状态, 而新 Session 只会读取成功写入的规则文件。

8.4 用户Abort ​

User reviewer 的 Abort 会让当前 turn 进入中断;Guardian 的取消则记录 policy denial。两者都让网络代理收到 Deny,但 诊断来源和上层 turn 终态不同。

9. 测试路径 ​

9.1 Host与端口 ​

网络集成测试先批准一个 host,随后使用相同 host、不同端口的命令,断言不同端口仍然触发审批;再使用不同 protocol,断言 HTTP 与 SOCKS5 不共享批准。测试输入直接验证四元 key,而不是只验证 UI 是否出现。

源码位置:codex-rs/core/tests/suite/network_approval.rs :: user_network_approval_once_session_and_denial_semantics

rust
let approval = expect_network_approval_target(
    &test,
    LOCAL_ENVIRONMENT_ID,
    &different_port_target,
    NetworkApprovalProtocol::Http,
)
.await?;
test.codex
    .submit(Op::ExecApproval {
        id: approval.effective_approval_id(),
        turn_id: Some(approval.turn_id),
        decision: ReviewDecision::denied("rejected by user"),
    })
    .await?;
wait_for_turn_complete(&test).await;

这证明端口和协议参与审批身份;不证明 DNS 别名、IP literal 或代理重定向会被视为同一 host。

9.2 并发Pending ​

网络单元测试构造相同 pending key 的多个请求,断言只有第一个请求成为 owner;owner drop 测试再验证 waiters 收到 Deny、关联 execution 被取消、旧 generation 不会删除新 pending。

相关测试:

  • codex-rs/core/src/tools/network_approval_tests.rs :: pending_approvals_are_deduped_within_one_execution
  • codex-rs/core/src/tools/network_approval_tests.rs :: dropping_pending_owner_denies_waiters_and_preserves_replacement

源码位置:codex-rs/core/src/tools/network_approval_tests.rs :: pending_approvals_are_deduped_within_one_execution

rust
let (first, first_is_owner) =
    service.get_or_create_pending_approval(key.clone());
let (second, second_is_owner) =
    service.get_or_create_pending_approval(key);

assert!(first_is_owner);
assert!(!second_is_owner);
assert!(Arc::ptr_eq(&first, &second));

对应的 owner-drop 测试还将两个 waiter 与一个 replacement pending 串起来,证明 Drop 清理不是简单 remove(key),而是需要 检查当前 Arc 是否仍属于这一代。

9.3 规则保存 ​

集成测试提交 allow amendment,检查 approval event 同时提供 allow/deny 两个 network amendments;决定返回后检查规则文件 包含 allow 记录,并检查后续相同 host 不再弹网络审批。另一个测试提交错误 host 的 amendment,断言当前网络请求仍被 policy block,错误 amendment 不会授权原 host。

源码位置:codex-rs/core/tests/suite/network_approval.rs :: allowing_network_policy_amendment_persists_context_and_bypasses_prompt

rust
let amendments = approval
    .proposed_network_policy_amendments
    .clone()
    .context("expected network policy amendments")?;
assert_eq!(
    amendments,
    vec![
        NetworkPolicyAmendment {
            host: NETWORK_TEST_HOST.to_string(),
            action: NetworkPolicyRuleAction::Allow,
        },
        NetworkPolicyAmendment {
            host: NETWORK_TEST_HOST.to_string(),
            action: NetworkPolicyRuleAction::Deny,
        },
    ]
);

test.codex
    .submit(Op::ExecApproval {
        id: approval.effective_approval_id(),
        turn_id: Some(approval.turn_id),
        decision: ReviewDecision::NetworkPolicyAmendment {
            network_policy_amendment: amendments[0].clone(),
        },
    })
    .await?;
wait_for_turn_complete(&test).await;
let policy = fs::read_to_string(test.home.path().join("rules/default.rules"))?;
assert!(policy.contains(
    r#"network_rule(host="codex-network-test.invalid", protocol="http", decision="allow""#
));

9.4 拒绝与取消 ​

测试还覆盖用户 Denied、用户 Abort 和 policy amendment deny:Denied 保持 turn 可继续,Abort 产生 TurnAborted,deny amendment 则把 host 写入 denied cache 并阻止后续请求。三条路径都不应被统称为“网络失败”。

10. 阅读实践 ​

在源码仓库根目录运行定向测试:

bash
RUST_MIN_STACK=16777216 cargo test -p codex-core network_approval
RUST_MIN_STACK=16777216 cargo test -p codex-core allowing_network_policy_amendment_persists_context_and_bypasses_prompt
RUST_MIN_STACK=16777216 cargo test -p codex-core dropping_pending_owner_denies_waiters_and_preserves_replacement

然后复述三个场景:

  1. 两个并发工具访问同一 host,但 execution id 不同。哪个结构负责避免错误归因,何时会拒绝自动归因?
  2. Deferred network approval 为什么要等到工具结果或进程退出后 finish?拒绝 outcome 在哪个生命周期点影响工具结果?
  3. 用户选择 allow amendment 后,哪些状态必须同时更新,规则文件写失败时哪些状态绝不能更新?

能够从代理 BlockedRequest 追到 record_blocked_request、handle_inline_policy_request、pending owner、规则保存和 finish_deferred_network_approval,就掌握了网络审批从阻断到执行结果的完整控制流。后续阅读可以回到工具审批架构, 对比通用 command approval 与 host-specific network approval 的共享层和差异层。