Scope extension capabilities to invocation lifetimes (#41020)

## What changed

- Add callback lifetimes to extension `ToolCall`, `ToolEnvironment`, turn-input context, and skill-read request types.
- Require extension tool executors to handle calls for any invocation lifetime and tie their returned futures to that lifetime.
- Update built-in extensions, adapters, and tests to use the lifetime-scoped APIs without retaining host-owned environment capabilities.

GitOrigin-RevId: aca9ce3a1b6870df550bc84ec9308cc65533f161
This commit is contained in:
Adam Perry @ OpenAI authored and copyberry committed 2026-08-27 02:00:31 +00:00
1 parent eed1dee69e
commit 81e180044d
85 files changed
+477 -202

No files matched your search

+6 -4
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::marker::PhantomData;
use std::sync::Arc;
use crate::HostSkillsSnapshot;
@@ -275,7 +276,7 @@ where
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
self.build_skill_tools(
session_store,
thread_store,
@@ -290,7 +291,7 @@ where
session_store: &ExtensionData,
thread_store: &ExtensionData,
step_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
let resolved_executor_roots = step_store
.get::<Vec<ResolvedSelectedCapabilityRoot>>()
.map(|roots| roots.as_slice().to_vec())
@@ -357,7 +358,7 @@ where
{
fn contribute<'a>(
&'a self,
input: TurnInputContext,
input: TurnInputContext<'a>,
extension_metrics: Option<Arc<dyn ExtensionMetrics>>,
session_store: &'a ExtensionData,
thread_store: &'a ExtensionData,
@@ -562,7 +563,7 @@ impl<C> SkillsExtension<C> {
executor_query: Option<SkillListQuery>,
selected_plugins: Option<Arc<SelectedPluginSnapshot>>,
sandbox_contexts: Option<Arc<HashMap<String, FileSystemSandboxContext>>>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
skill_tools(
self.providers.clone(),
session_store,
@@ -606,6 +607,7 @@ impl<C> SkillsExtension<C> {
.read_skill(
&self.providers,
SkillReadRequest {
_lifetime: PhantomData,
authority: entry.authority.clone(),
package: entry.id.clone(),
resource: entry.main_prompt.clone(),
+8 -2
View File
@@ -1,4 +1,5 @@
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
@@ -43,7 +44,9 @@ pub struct SkillListQuery {
}
#[derive(Clone, Debug)]
pub struct SkillReadRequest {
pub struct SkillReadRequest<'a> {
// TODO(anp): Replace the marker with callback-scoped environment access.
pub _lifetime: PhantomData<&'a ()>,
pub authority: SkillAuthority,
pub package: SkillPackageId,
pub resource: SkillResourceId,
@@ -71,7 +74,10 @@ pub type SkillProviderFuture<'a, T> =
pub trait SkillProvider: Send + Sync {
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog>;
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult>;
fn read<'a>(
&'a self,
request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult>;
fn search(&self, request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult>;
}
+4 -1
View File
@@ -121,7 +121,10 @@ impl SkillProvider for ExecutorSkillProvider {
})
}
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
fn read<'a>(
&'a self,
request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult> {
Box::pin(async move {
if request.authority.kind != SkillSourceKind::Executor {
return Err(SkillProviderError::new(format!(
+4 -1
View File
@@ -46,7 +46,10 @@ impl SkillProvider for HostSkillProvider {
})
}
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
fn read<'a>(
&'a self,
request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult> {
Box::pin(async move {
let Some(host_snapshot) = request.host_snapshot else {
return Err(SkillProviderError::new(
@@ -174,7 +174,10 @@ impl SkillProvider for OrchestratorSkillProvider {
})
}
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
fn read<'a>(
&'a self,
request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult> {
Box::pin(async move {
if request.authority
!= SkillAuthority::new(SkillSourceKind::Orchestrator, CODEX_APPS_MCP_SERVER_NAME)
@@ -69,7 +69,7 @@ impl ShadowSelectionExperiment {
pub(crate) fn run(
&self,
input: &TurnInputContext,
input: &TurnInputContext<'_>,
catalog: &SkillCatalog,
explicitly_selected: &[SkillCatalogEntry],
host_snapshot: Option<&HostSkillsSnapshot>,
+1 -1
View File
@@ -171,7 +171,7 @@ impl SkillProviders {
pub(crate) async fn read(
&self,
request: SkillReadRequest,
request: SkillReadRequest<'_>,
) -> Result<SkillReadResult, SkillProviderError> {
let mut last_error = None;
for source in self
+3 -3
View File
@@ -222,7 +222,7 @@ impl SkillsThreadState {
pub(crate) async fn read_skill(
&self,
providers: &SkillProviders,
request: SkillReadRequest,
request: SkillReadRequest<'_>,
) -> SkillProviderResult<SkillReadResult> {
if request.authority.kind != SkillSourceKind::Orchestrator {
return providers.read(request).await;
@@ -349,8 +349,8 @@ struct SkillReadCacheKey {
resource: SkillResourceId,
}
impl From<&SkillReadRequest> for SkillReadCacheKey {
fn from(request: &SkillReadRequest) -> Self {
impl From<&SkillReadRequest<'_>> for SkillReadCacheKey {
fn from(request: &SkillReadRequest<'_>) -> Self {
Self {
authority: request.authority.clone(),
package: request.package.clone(),
+5 -2
View File
@@ -63,7 +63,7 @@ pub(super) struct ListTool {
pub(super) context: SkillToolContext,
}
impl ToolExecutor<ToolCall> for ListTool {
impl<'call> ToolExecutor<ToolCall<'call>> for ListTool {
fn tool_name(&self) -> ToolName {
skill_tool_name(TOOL_NAME)
}
@@ -75,7 +75,10 @@ impl ToolExecutor<ToolCall> for ListTool {
)
}
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> {
fn handle<'a>(&'a self, call: ToolCall<'call>) -> ToolExecutorFuture<'a>
where
'call: 'a,
{
Box::pin(async move {
let args: ListArgs = parse_args(&call)?;
let response_byte_budget = call.response_byte_budget(MAX_SKILL_RESPONSE_BYTES);
+2 -2
View File
@@ -63,7 +63,7 @@ pub(crate) fn skill_tools(
selected_plugins: Option<Arc<SelectedPluginSnapshot>>,
sandbox_contexts: Option<Arc<HashMap<String, FileSystemSandboxContext>>>,
shadow_selection: Arc<ShadowSelectionExperiment>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
) -> Vec<Arc<dyn for<'call> ToolExecutor<ToolCall<'call>>>> {
let Some(thread_state) = thread_store.get::<SkillsThreadState>() else {
return Vec::new();
};
@@ -314,7 +314,7 @@ fn skill_function_tool<I: JsonSchema, O: JsonSchema>(name: &str, description: &s
})
}
fn parse_args<T: for<'de> Deserialize<'de>>(call: &ToolCall) -> Result<T, FunctionCallError> {
fn parse_args<T: for<'de> Deserialize<'de>>(call: &ToolCall<'_>) -> Result<T, FunctionCallError> {
let arguments = call.function_arguments()?;
let value = if arguments.trim().is_empty() {
Value::Object(serde_json::Map::new())
+7 -2
View File
@@ -1,3 +1,4 @@
use std::marker::PhantomData;
use std::sync::Arc;
use codex_analytics::InvocationType;
@@ -54,7 +55,7 @@ pub(super) struct ReadTool {
pub(super) context: SkillToolContext,
}
impl ToolExecutor<ToolCall> for ReadTool {
impl<'call> ToolExecutor<ToolCall<'call>> for ReadTool {
fn tool_name(&self) -> ToolName {
skill_tool_name(TOOL_NAME)
}
@@ -66,7 +67,10 @@ impl ToolExecutor<ToolCall> for ReadTool {
)
}
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_> {
fn handle<'a>(&'a self, call: ToolCall<'call>) -> ToolExecutorFuture<'a>
where
'call: 'a,
{
Box::pin(async move {
let args: ReadArgs = parse_args(&call)?;
let response_byte_budget = call.response_byte_budget(MAX_SKILL_RESPONSE_BYTES);
@@ -183,6 +187,7 @@ impl ToolExecutor<ToolCall> for ReadTool {
.read_skill(
&self.context.providers,
SkillReadRequest {
_lifetime: PhantomData,
authority: authority.clone(),
package: package.clone(),
resource: requested_resource.clone(),
@@ -1,5 +1,6 @@
use std::collections::HashMap;
use std::io;
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
@@ -318,6 +319,7 @@ async fn windows_executor_skill_read_rejects_disabled_sandbox_on_any_orchestrato
);
let error = provider
.read(SkillReadRequest {
_lifetime: PhantomData,
authority: SkillAuthority::new(SkillSourceKind::Executor, "windows-root"),
package: SkillPackageId("skill://windows-root/C:/skill".into()),
resource,
@@ -876,6 +878,7 @@ async fn high_level_discovery_reuses_materialized_skill_contents_for_reads() {
panic!("expected exactly one skill");
};
let request = SkillReadRequest {
_lifetime: PhantomData,
authority: entry.authority.clone(),
package: entry.id.clone(),
resource: entry.main_prompt.clone(),
+12 -13
View File
@@ -2449,7 +2449,7 @@ async fn prompt_hidden_skill_can_still_be_invoked() -> TestResult {
#[derive(Clone)]
struct StaticSkillProvider {
catalog: SkillCatalog,
read_requests: Arc<Mutex<Vec<SkillReadRequest>>>,
read_requests: Arc<Mutex<Vec<(SkillAuthority, SkillPackageId, SkillResourceId)>>>,
list_calls: Option<Arc<AtomicUsize>>,
fail_first_list: bool,
}
@@ -2565,13 +2565,20 @@ impl SkillProvider for StaticSkillProvider {
})
}
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
fn read<'a>(
&'a self,
request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult> {
let read_requests = Arc::clone(&self.read_requests);
Box::pin(async move {
read_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(request.clone());
.push((
request.authority.clone(),
request.package.clone(),
request.resource.clone(),
));
Ok(SkillReadResult {
resource: request.resource,
contents: "# Lint Fix\n\nRun the formatter.".to_string(),
@@ -2637,18 +2644,10 @@ fn test_codex_home() -> PathBuf {
}
fn read_request_keys(
requests: &Arc<Mutex<Vec<SkillReadRequest>>>,
requests: &Mutex<Vec<(SkillAuthority, SkillPackageId, SkillResourceId)>>,
) -> Vec<(SkillAuthority, SkillPackageId, SkillResourceId)> {
requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.map(|request| {
(
request.authority.clone(),
request.package.clone(),
request.resource.clone(),
)
})
.collect()
.clone()
}
@@ -11,7 +11,10 @@ impl SkillProvider for FailedReads {
self.0.list(query)
}
fn read(&self, _request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult> {
fn read<'a>(
&'a self,
_request: SkillReadRequest<'a>,
) -> SkillProviderFuture<'a, SkillReadResult> {
Box::pin(async { Err(SkillProviderError::new("read unavailable")) })
}