This commit is contained in:
jif-oai
2026-05-07 13:23:11 +01:00
parent f7e8ff8e50
commit 1af814fb51
19 changed files with 710 additions and 0 deletions
+35
View File
@@ -2780,6 +2780,16 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "codex-extension-api"
version = "0.0.0"
dependencies = [
"codex-git-attribution",
"codex-memories",
"codex-multi-agent-v2",
"rmcp",
]
[[package]]
name = "codex-external-agent-migration"
version = "0.0.0"
@@ -2858,6 +2868,13 @@ dependencies = [
"serde",
]
[[package]]
name = "codex-git-attribution"
version = "0.0.0"
dependencies = [
"codex-extension-api",
]
[[package]]
name = "codex-git-utils"
version = "0.0.0"
@@ -3065,6 +3082,17 @@ dependencies = [
"wiremock",
]
[[package]]
name = "codex-memories"
version = "0.0.0"
dependencies = [
"codex-extension-api",
"codex-memories-read",
"codex-utils-absolute-path",
"rmcp",
"serde_json",
]
[[package]]
name = "codex-memories-mcp"
version = "0.0.0"
@@ -3206,6 +3234,13 @@ dependencies = [
"tracing",
]
[[package]]
name = "codex-multi-agent-v2"
version = "0.0.0"
dependencies = [
"codex-extension-api",
]
[[package]]
name = "codex-network-proxy"
version = "0.0.0"
+8
View File
@@ -45,6 +45,10 @@ members = [
"exec-server",
"execpolicy",
"execpolicy-legacy",
"ext/extension-api",
"ext/git-attribution",
"ext/memories",
"ext/multi-agent-v2",
"external-agent-migration",
"external-agent-sessions",
"keyring-store",
@@ -159,10 +163,14 @@ codex-exec = { path = "exec" }
codex-file-system = { path = "file-system" }
codex-exec-server = { path = "exec-server" }
codex-execpolicy = { path = "execpolicy" }
codex-extension-api = { path = "ext/extension-api" }
codex-external-agent-migration = { path = "external-agent-migration" }
codex-external-agent-sessions = { path = "external-agent-sessions" }
codex-experimental-api-macros = { path = "codex-experimental-api-macros" }
codex-features = { path = "features" }
codex-git-attribution = { path = "ext/git-attribution" }
codex-memories = { path = "ext/memories" }
codex-multi-agent-v2 = { path = "ext/multi-agent-v2" }
codex-feedback = { path = "feedback" }
codex-install-context = { path = "install-context" }
codex-file-search = { path = "file-search" }
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "extension-api",
crate_name = "codex_extension_api",
)
+23
View File
@@ -0,0 +1,23 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-extension-api"
version.workspace = true
[lib]
name = "codex_extension_api"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
rmcp = { workspace = true, default-features = false, features = [
"schemars",
"server",
] }
[dev-dependencies]
codex-git-attribution = { workspace = true }
codex-memories = { workspace = true }
codex-multi-agent-v2 = { workspace = true }
@@ -0,0 +1,113 @@
use std::sync::Arc;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_git_attribution as git_attribution;
use codex_git_attribution::GitAttributionContext;
use codex_memories::MemoriesContext;
use codex_memories::MemoriesExtension;
use codex_multi_agent_v2 as multi_agent_v2;
use codex_multi_agent_v2::MultiAgentV2Context;
use codex_multi_agent_v2::UsageHintAudience;
fn main() {
let registry = ExtensionRegistryBuilder::<ctx::RuntimeContext>::new()
.with_extension(git_attribution::extension())
.with_extension(Arc::new(MemoriesExtension::new(Some(
"Please use FS access bla bla bla.".to_string(),
))))
.with_extension(multi_agent_v2::extension())
.build();
let root_context = ctx::RuntimeContext {
commit_attribution: None,
memory_tool_enabled: true,
use_memories: true,
multi_agent_v2_enabled: true,
multi_agent_v2_usage_hint_audience: UsageHintAudience::RootAgent,
root_agent_usage_hint_text: Some("Root-agent usage hint.".to_string()),
subagent_usage_hint_text: Some("Subagent usage hint.".to_string()),
};
let memories_disabled_context = ctx::RuntimeContext {
use_memories: false,
..root_context.clone()
};
// Build the prompt
let prompt_fragments = registry
.prompt_contributors()
.iter()
.flat_map(|contributor| contributor.contribute(&root_context))
.collect::<Vec<_>>();
// Get tools (MCP here but this should shift to handlers)
let tools = registry
.mcp_tool_contributors()
.iter()
.flat_map(|contributor| contributor.tools(&root_context))
.collect::<Vec<_>>();
let tools_without_memories = registry
.mcp_tool_contributors()
.iter()
.flat_map(|contributor| contributor.tools(&memories_disabled_context))
.collect::<Vec<_>>();
println!("prompt fragments: {}", prompt_fragments.len());
println!("mcp tools: {}", tools.len());
println!(
"mcp tools when use_memories=false: {}",
tools_without_memories.len()
);
}
mod ctx {
use codex_git_attribution::GitAttributionContext;
use codex_memories::MemoriesContext;
use codex_multi_agent_v2::{MultiAgentV2Context, UsageHintAudience};
#[derive(Clone)]
pub struct RuntimeContext {
// Ideally this should be at the config layer instead
pub commit_attribution: Option<String>,
pub memory_tool_enabled: bool,
pub use_memories: bool,
pub multi_agent_v2_enabled: bool,
pub multi_agent_v2_usage_hint_audience: UsageHintAudience,
pub root_agent_usage_hint_text: Option<String>,
pub subagent_usage_hint_text: Option<String>,
}
impl GitAttributionContext for RuntimeContext {
fn commit_attribution(&self) -> Option<&str> {
self.commit_attribution.as_deref()
}
}
impl MemoriesContext for RuntimeContext {
fn memory_tool_enabled(&self) -> bool {
self.memory_tool_enabled
}
fn use_memories(&self) -> bool {
self.use_memories
}
}
impl MultiAgentV2Context for RuntimeContext {
fn multi_agent_v2_enabled(&self) -> bool {
self.multi_agent_v2_enabled
}
fn usage_hint_audience(&self) -> UsageHintAudience {
self.multi_agent_v2_usage_hint_audience
}
fn root_agent_usage_hint_text(&self) -> Option<&str> {
self.root_agent_usage_hint_text.as_deref()
}
fn subagent_usage_hint_text(&self) -> Option<&str> {
self.subagent_usage_hint_text.as_deref()
}
}
}
@@ -0,0 +1,36 @@
//! Runtime contribution ports exposed by the extension API.
//!
//! Extensions can install several independent contribution types. This module
//! is the small public façade over those ports: callers can import the common
//! vocabulary from one place, while each contribution family keeps its own
//! supporting types nearby.
mod prompt;
pub use prompt::PromptFragment;
pub use prompt::PromptSlot;
use rmcp::model::Tool;
/// Extension contribution that adds prompt fragments during prompt assembly.
///
/// Implementations should inspect only their feature-owned slice of the
/// current runtime context and describe the prompt content exposed for that
/// invocation. The host remains responsible for ordering fragments and
/// assembling prompt items.
pub trait PromptContributor<C>: Send + Sync {
fn contribute(&self, context: &C) -> Vec<PromptFragment>;
}
/// Extension contribution that exposes MCP tool definitions owned by a feature.
///
/// Implementations should inspect only their feature-owned slice of the
/// current runtime context and return the tools exposed for that invocation.
/// The host remains responsible for mounting those tools and routing
/// execution.
///
/// This is intentionally MCP-shaped for now because the more general tool
/// abstraction has not been extracted yet.
pub trait McpToolContributor<C>: Send + Sync {
fn tools(&self, context: &C) -> Vec<Tool>;
}
@@ -0,0 +1,50 @@
/// Prompt region targeted by a contributed fragment.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PromptSlot {
DeveloperPolicy,
DeveloperCapabilities,
ContextualUser,
SeparateDeveloper,
}
/// One text fragment contributed to prompt assembly by an extension. (wiring not done yet on purpose)
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromptFragment {
slot: PromptSlot,
text: String,
}
impl PromptFragment {
/// Creates a prompt fragment for the given slot.
pub fn new(slot: PromptSlot, text: impl Into<String>) -> Self {
Self {
slot,
text: text.into(),
}
}
/// Creates a developer-policy prompt fragment.
pub fn developer_policy(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperPolicy, text)
}
/// Creates a developer-capabilities prompt fragment.
pub fn developer_capability(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperCapabilities, text)
}
/// Creates a separate top-level developer prompt fragment.
pub fn separate_developer(text: impl Into<String>) -> Self {
Self::new(PromptSlot::SeparateDeveloper, text)
}
/// Returns the target prompt slot.
pub fn slot(&self) -> PromptSlot {
self.slot
}
/// Returns the model-visible text.
pub fn text(&self) -> &str {
&self.text
}
}
@@ -0,0 +1,12 @@
use std::sync::Arc;
use crate::ExtensionRegistryBuilder;
/// First-party extension that can install one or more typed runtime contributions.
///
/// Implementations should use [`Self::install`] only to register the concrete
/// providers they own.
pub trait CodexExtension<C>: Send + Sync {
/// Registers this extension's concrete typed contributions.
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>);
}
+20
View File
@@ -0,0 +1,20 @@
//! Semantic ports and typed assembly for first-party Codex extensions.
//!
//! This crate owns the small stable assembly layer: host-independent semantic
//! ports plus the registry used to collect their implementations. Port
//! contracts should live here only once they can be expressed without pulling
//! `codex-core` internals across the boundary.
#![forbid(unsafe_code)]
mod contributors;
mod extension;
mod registry;
pub use contributors::McpToolContributor;
pub use contributors::PromptContributor;
pub use contributors::PromptFragment;
pub use contributors::PromptSlot;
pub use extension::CodexExtension;
pub use registry::ExtensionRegistry;
pub use registry::ExtensionRegistryBuilder;
@@ -0,0 +1,81 @@
use std::sync::Arc;
use crate::CodexExtension;
use crate::McpToolContributor;
use crate::PromptContributor;
/// Mutable registry used while extensions install their typed contributions.
pub struct ExtensionRegistryBuilder<C> {
mcp_tool_contributors: Vec<Arc<dyn McpToolContributor<C>>>,
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
}
impl<C> Default for ExtensionRegistryBuilder<C> {
fn default() -> Self {
Self {
mcp_tool_contributors: Vec::new(),
prompt_contributors: Vec::new(),
}
}
}
impl<C> ExtensionRegistryBuilder<C> {
/// Creates an empty registry builder.
pub fn new() -> Self {
Self::default()
}
/// Installs one extension and returns the builder.
#[must_use]
pub fn with_extension<E>(mut self, extension: Arc<E>) -> Self
where
E: CodexExtension<C> + 'static,
{
self.install_extension(extension);
self
}
/// Installs one extension into the registry under construction.
pub fn install_extension<E>(&mut self, extension: Arc<E>)
where
E: CodexExtension<C> + 'static,
{
extension.install(self);
}
/// Registers one MCP tool contributor.
pub fn mcp_tool_contributor(&mut self, contributor: Arc<dyn McpToolContributor<C>>) {
self.mcp_tool_contributors.push(contributor);
}
/// Registers one prompt contributor.
pub fn prompt_contributor(&mut self, contributor: Arc<dyn PromptContributor<C>>) {
self.prompt_contributors.push(contributor);
}
/// Finishes construction and returns the immutable registry.
pub fn build(self) -> ExtensionRegistry<C> {
ExtensionRegistry {
mcp_tool_contributors: self.mcp_tool_contributors,
prompt_contributors: self.prompt_contributors,
}
}
}
/// Immutable typed registry produced after extensions are installed.
pub struct ExtensionRegistry<C> {
mcp_tool_contributors: Vec<Arc<dyn McpToolContributor<C>>>,
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
}
impl<C> ExtensionRegistry<C> {
/// Returns the registered MCP tool contributors.
pub fn mcp_tool_contributors(&self) -> &[Arc<dyn McpToolContributor<C>>] {
&self.mcp_tool_contributors
}
/// Returns the registered prompt contributors.
pub fn prompt_contributors(&self) -> &[Arc<dyn PromptContributor<C>>] {
&self.prompt_contributors
}
}
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "git-attribution",
crate_name = "codex_git_attribution",
)
+15
View File
@@ -0,0 +1,15 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-git-attribution"
version.workspace = true
[lib]
name = "codex_git_attribution"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
codex-extension-api = { workspace = true }
+79
View File
@@ -0,0 +1,79 @@
//! Git-attribution prompt contribution packaged as a Codex extension.
#![forbid(unsafe_code)]
use std::sync::Arc;
use codex_extension_api::CodexExtension;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
const DEFAULT_ATTRIBUTION_VALUE: &str = "Codex <noreply@openai.com>";
/// Runtime facts needed to render the git-attribution prompt contribution.
///
/// Hosts should return the current effective attribution override for the
/// thread being assembled. `None` selects Codex's default attribution, while a
/// blank string disables the contribution.
pub trait GitAttributionContext {
fn commit_attribution(&self) -> Option<&str>;
}
/// Prompt-only extension that contributes the configured git-attribution instruction.
#[derive(Clone, Copy, Debug, Default)]
pub struct GitAttributionExtension;
impl GitAttributionExtension {
/// Creates an extension instance.
pub fn new() -> Self {
Self
}
/// Returns the model-visible trailer instruction, if attribution is enabled.
pub fn instruction<C: GitAttributionContext>(&self, context: &C) -> Option<String> {
let trailer = build_commit_message_trailer(context.commit_attribution())?;
Some(format!(
"When you write or edit a git commit message, ensure the message ends with this trailer exactly once:\n{trailer}\n\nRules:\n- Keep existing trailers and append this trailer at the end if missing.\n- Do not duplicate this trailer if it already exists.\n- Keep one blank line between the commit body and trailer block."
))
}
}
impl<C: GitAttributionContext> PromptContributor<C> for GitAttributionExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
self.instruction(context)
.map(PromptFragment::developer_capability)
.into_iter()
.collect()
}
}
impl<C: GitAttributionContext> CodexExtension<C> for GitAttributionExtension {
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
registry.prompt_contributor(self);
}
}
/// Creates a shared git-attribution extension instance.
pub fn extension() -> Arc<GitAttributionExtension> {
Arc::new(GitAttributionExtension::new())
}
fn build_commit_message_trailer(config_attribution: Option<&str>) -> Option<String> {
let value = resolve_attribution_value(config_attribution)?;
Some(format!("Co-authored-by: {value}"))
}
fn resolve_attribution_value(config_attribution: Option<&str>) -> Option<String> {
match config_attribution {
Some(value) => {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
None => Some(DEFAULT_ATTRIBUTION_VALUE.to_string()),
}
}
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "memories",
crate_name = "codex_memories",
)
+22
View File
@@ -0,0 +1,22 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-memories"
version.workspace = true
[lib]
name = "codex_memories"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
codex-extension-api = { workspace = true }
codex-memories-read = { workspace = true }
codex-utils-absolute-path = { workspace = true }
rmcp = { workspace = true, default-features = false, features = [
"schemars",
"server",
] }
serde_json = { workspace = true }
+103
View File
@@ -0,0 +1,103 @@
//! Memories tools and prompt contribution packaged as a Codex extension.
#![forbid(unsafe_code)]
use std::sync::Arc;
use codex_extension_api::CodexExtension;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::McpToolContributor;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
use codex_memories_read::build_memory_tool_developer_instructions;
use codex_utils_absolute_path::AbsolutePathBuf;
use rmcp::model::Tool;
use rmcp::model::ToolAnnotations;
use serde_json::Map;
use serde_json::Value;
/// Runtime facts needed to decide whether read-memory surfaces are visible.
///
/// Hosts should expose the current effective values for the thread being
/// assembled. The extension owns the policy that combines those values.
pub trait MemoriesContext {
fn memory_tool_enabled(&self) -> bool;
fn use_memories(&self) -> bool;
}
/// Extension that contributes memories MCP tools plus their read-path guidance.
#[derive(Clone, Debug)]
pub struct MemoriesExtension {
read_prompt: Option<String>,
}
impl MemoriesExtension {
/// Creates an extension from a pre-rendered read prompt.
pub fn new(read_prompt: Option<String>) -> Self {
Self { read_prompt }
}
/// Creates an extension using the live memories read prompt for this Codex home.
pub async fn from_codex_home(codex_home: &AbsolutePathBuf) -> Self {
Self {
read_prompt: build_memory_tool_developer_instructions(codex_home).await,
}
}
/// Returns the rendered developer instruction for read access, if available.
pub fn read_prompt(&self) -> Option<&str> {
self.read_prompt.as_deref()
}
fn is_read_surface_enabled<C: MemoriesContext>(&self, context: &C) -> bool {
context.memory_tool_enabled() && context.use_memories()
}
}
impl<C: MemoriesContext> McpToolContributor<C> for MemoriesExtension {
fn tools(&self, context: &C) -> Vec<Tool> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
vec![
simple_tool("list", "List memory entries."),
simple_tool("read", "Read one memory entry."),
simple_tool("search", "Search memory entries."),
]
}
}
impl<C: MemoriesContext> PromptContributor<C> for MemoriesExtension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
if !self.is_read_surface_enabled(context) {
return Vec::new();
}
self.read_prompt()
.map(PromptFragment::developer_policy)
.into_iter()
.collect()
}
}
impl<C: MemoriesContext> CodexExtension<C> for MemoriesExtension {
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
registry.mcp_tool_contributor(self.clone());
registry.prompt_contributor(self);
}
}
/// Creates a shared memories extension using the live memories read prompt.
pub async fn extension(codex_home: &AbsolutePathBuf) -> Arc<MemoriesExtension> {
Arc::new(MemoriesExtension::from_codex_home(codex_home).await)
}
fn simple_tool(name: &'static str, description: &'static str) -> Tool {
Tool::new(name, description, simple_input_schema())
.annotate(ToolAnnotations::new().read_only(true))
}
fn simple_input_schema() -> Map<String, Value> {
Map::from_iter([("type".to_string(), Value::String("object".to_string()))])
}
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "multi-agent-v2",
crate_name = "codex_multi_agent_v2",
)
+15
View File
@@ -0,0 +1,15 @@
[package]
edition.workspace = true
license.workspace = true
name = "codex-multi-agent-v2"
version.workspace = true
[lib]
name = "codex_multi_agent_v2"
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
codex-extension-api = { workspace = true }
+74
View File
@@ -0,0 +1,74 @@
//! MultiAgentV2 usage-hint prompt contribution packaged as a Codex extension.
#![forbid(unsafe_code)]
use std::sync::Arc;
use codex_extension_api::CodexExtension;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::PromptContributor;
use codex_extension_api::PromptFragment;
/// Which kind of agent should receive a MultiAgentV2 usage hint.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UsageHintAudience {
RootAgent,
SpawnedSubagent,
Omitted,
}
/// Runtime facts needed to render MultiAgentV2 usage-hint contributions.
///
/// Hosts should return the current effective settings for the thread being
/// assembled. The extension owns the routing policy that chooses which hint, if
/// any, is visible for that invocation.
pub trait MultiAgentV2Context {
fn multi_agent_v2_enabled(&self) -> bool;
fn usage_hint_audience(&self) -> UsageHintAudience;
fn root_agent_usage_hint_text(&self) -> Option<&str>;
fn subagent_usage_hint_text(&self) -> Option<&str>;
}
/// Prompt-only extension that contributes one MultiAgentV2 usage hint when enabled.
#[derive(Clone, Copy, Debug, Default)]
pub struct MultiAgentV2Extension;
impl MultiAgentV2Extension {
/// Creates an extension instance.
pub fn new() -> Self {
Self
}
/// Returns the usage hint text that should be shown for this agent, if any.
pub fn usage_hint_text<'a, C: MultiAgentV2Context>(&self, context: &'a C) -> Option<&'a str> {
if !context.multi_agent_v2_enabled() {
return None;
}
match context.usage_hint_audience() {
UsageHintAudience::RootAgent => context.root_agent_usage_hint_text(),
UsageHintAudience::SpawnedSubagent => context.subagent_usage_hint_text(),
UsageHintAudience::Omitted => None,
}
}
}
impl<C: MultiAgentV2Context> PromptContributor<C> for MultiAgentV2Extension {
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
self.usage_hint_text(context)
.map(PromptFragment::separate_developer)
.into_iter()
.collect()
}
}
impl<C: MultiAgentV2Context> CodexExtension<C> for MultiAgentV2Extension {
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
registry.prompt_contributor(self);
}
}
/// Creates a shared MultiAgentV2 extension instance.
pub fn extension() -> Arc<MultiAgentV2Extension> {
Arc::new(MultiAgentV2Extension::new())
}