extension-api: add approval review contributor flow (#22344)

## Why

`codex-extension-api` needs an approval hook that lets an installed
extension own a rendered approval-review prompt and produce the final
`ReviewDecision`. The prior interceptor stub only exposed a yes/no claim
and did not model the review result itself, which left the host with the
missing half of the control flow.

## What changed

- Replaces `ApprovalInterceptorContributor` with
[`ApprovalReviewContributor`](https://github.com/openai/codex/blob/c49d17531e15057a373a9b17f410cafb6299d0c1/codex-rs/ext/extension-api/src/contributors.rs#L43-L55),
which may claim a rendered prompt and return an async `ReviewDecision`.
- Re-exports the new contributor and future types from `extension-api`.
- Adds registry support through `approval_review_contributor(...)` plus
[`ExtensionRegistry::approval_review(...)`](https://github.com/openai/codex/blob/c49d17531e15057a373a9b17f410cafb6299d0c1/codex-rs/ext/extension-api/src/registry.rs#L90-L101),
which returns the first installed contributor that claims the prompt.
This commit is contained in:
jif-oai
2026-05-13 10:39:12 +02:00
committed by GitHub
Unverified
parent 7e97da7c13
commit 155c04ad40
3 changed files with 38 additions and 27 deletions
+15 -12
View File
@@ -2,6 +2,7 @@ use std::future::Future;
use codex_protocol::ThreadId;
use codex_protocol::items::TurnItem;
use codex_protocol::protocol::ReviewDecision;
use codex_tool_api::ToolBundle;
use crate::ExtensionData;
@@ -39,6 +40,20 @@ pub trait ToolContributor: Send + Sync {
-> Vec<ToolBundle>;
}
/// Future returned by one claimed approval-review contribution.
pub type ApprovalReviewFuture<'a> =
std::pin::Pin<Box<dyn Future<Output = ReviewDecision> + Send + 'a>>;
/// Extension contribution that can claim rendered approval-review prompts.
pub trait ApprovalReviewContributor: Send + Sync {
fn contribute<'a>(
&'a self,
session_store: &'a ExtensionData,
thread_store: &'a ExtensionData,
prompt: &'a str,
) -> Option<ApprovalReviewFuture<'a>>;
}
/// Future returned by one ordered turn-item contribution.
pub type TurnItemContributionFuture<'a> =
std::pin::Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
@@ -56,15 +71,3 @@ pub trait TurnItemContributor: Send + Sync {
item: &'a mut TurnItem,
) -> TurnItemContributionFuture<'a>;
}
// TODO: WIP (do not consider)
/// Extension contribution that can claim approval requests for a runtime context.
/// (ideally we can replace it by a session lifecycle thing or a request contributor?)
pub trait ApprovalInterceptorContributor: Send + Sync {
/// Returns whether this contributor should intercept approvals in `context`.
fn intercepts_approvals(
&self,
thread_store: &ExtensionData,
turn_store: &ExtensionData,
) -> bool;
}
+2 -1
View File
@@ -11,7 +11,8 @@ pub use codex_tool_api::ToolCall;
pub use codex_tool_api::ToolError;
pub use codex_tool_api::ToolExecutor;
pub use codex_tool_api::ToolFuture;
pub use contributors::ApprovalInterceptorContributor;
pub use contributors::ApprovalReviewContributor;
pub use contributors::ApprovalReviewFuture;
pub use contributors::ContextContributor;
pub use contributors::PromptFragment;
pub use contributors::PromptSlot;
+21 -14
View File
@@ -1,7 +1,9 @@
use std::sync::Arc;
use crate::ApprovalInterceptorContributor;
use crate::ApprovalReviewContributor;
use crate::ApprovalReviewFuture;
use crate::ContextContributor;
use crate::ExtensionData;
use crate::ThreadStartContributor;
use crate::ToolContributor;
use crate::TurnItemContributor;
@@ -12,14 +14,14 @@ pub struct ExtensionRegistryBuilder<C> {
context_contributors: Vec<Arc<dyn ContextContributor>>,
tool_contributors: Vec<Arc<dyn ToolContributor>>,
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor>>,
approval_review_contributors: Vec<Arc<dyn ApprovalReviewContributor>>,
}
impl<C> Default for ExtensionRegistryBuilder<C> {
fn default() -> Self {
Self {
thread_start_contributors: Vec::new(),
approval_interceptor_contributors: Vec::new(),
approval_review_contributors: Vec::new(),
context_contributors: Vec::new(),
tool_contributors: Vec::new(),
turn_item_contributors: Vec::new(),
@@ -33,12 +35,9 @@ impl<C> ExtensionRegistryBuilder<C> {
Self::default()
}
/// Registers one approval interceptor contributor.
pub fn approval_interceptor_contributor(
&mut self,
contributor: Arc<dyn ApprovalInterceptorContributor>,
) {
self.approval_interceptor_contributors.push(contributor);
/// Registers one approval-review contributor.
pub fn approval_review_contributor(&mut self, contributor: Arc<dyn ApprovalReviewContributor>) {
self.approval_review_contributors.push(contributor);
}
/// Registers one thread-start contributor.
@@ -65,7 +64,7 @@ impl<C> ExtensionRegistryBuilder<C> {
pub fn build(self) -> ExtensionRegistry<C> {
ExtensionRegistry {
thread_start_contributors: self.thread_start_contributors,
approval_interceptor_contributors: self.approval_interceptor_contributors,
approval_review_contributors: self.approval_review_contributors,
context_contributors: self.context_contributors,
tool_contributors: self.tool_contributors,
turn_item_contributors: self.turn_item_contributors,
@@ -79,7 +78,7 @@ pub struct ExtensionRegistry<C> {
context_contributors: Vec<Arc<dyn ContextContributor>>,
tool_contributors: Vec<Arc<dyn ToolContributor>>,
turn_item_contributors: Vec<Arc<dyn TurnItemContributor>>,
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor>>,
approval_review_contributors: Vec<Arc<dyn ApprovalReviewContributor>>,
}
impl<C> ExtensionRegistry<C> {
@@ -88,9 +87,17 @@ impl<C> ExtensionRegistry<C> {
&self.thread_start_contributors
}
/// Returns the registered approval interceptor contributors.
pub fn approval_interceptor_contributors(&self) -> &[Arc<dyn ApprovalInterceptorContributor>] {
&self.approval_interceptor_contributors
/// Claims the first rendered approval-review prompt accepted by an
/// installed contributor.
pub fn approval_review<'a>(
&'a self,
session_store: &'a ExtensionData,
thread_store: &'a ExtensionData,
prompt: &'a str,
) -> Option<ApprovalReviewFuture<'a>> {
self.approval_review_contributors
.iter()
.find_map(|contributor| contributor.contribute(session_store, thread_store, prompt))
}
/// Returns the registered prompt contributors.