mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
v2
This commit is contained in:
@@ -12,12 +12,12 @@ path = "src/lib.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
codex-tools = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
codex-guardian = { workspace = true }
|
||||
codex-git-attribution = { workspace = true }
|
||||
codex-memories = { workspace = true }
|
||||
codex-multi-agent-v2 = { workspace = true }
|
||||
|
||||
@@ -2,23 +2,27 @@ 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_guardian as guardian;
|
||||
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(guardian::extension())
|
||||
.with_extension(git_attribution::extension())
|
||||
.with_extension(Arc::new(MemoriesExtension::new(Some(
|
||||
.with_extension(Arc::new(MemoriesExtension::with_read_prompt(
|
||||
"Please use FS access bla bla bla.".to_string(),
|
||||
))))
|
||||
std::env::temp_dir().join("codex-memories-example"),
|
||||
)))
|
||||
.with_extension(multi_agent_v2::extension())
|
||||
.build();
|
||||
|
||||
let root_context = ctx::RuntimeContext {
|
||||
automatic_review_enabled: true,
|
||||
approval_policy_allows_automatic_review: true,
|
||||
is_guardian_reviewer: false,
|
||||
guardian_policy_prompt: Some("Guardian policy.".to_string()),
|
||||
commit_attribution: None,
|
||||
memory_tool_enabled: true,
|
||||
use_memories: true,
|
||||
@@ -39,34 +43,44 @@ fn main() {
|
||||
.flat_map(|contributor| contributor.contribute(&root_context))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Get tools (MCP here but this should shift to handlers)
|
||||
// Get native tools
|
||||
let tools = registry
|
||||
.mcp_tool_contributors()
|
||||
.tool_contributors()
|
||||
.iter()
|
||||
.flat_map(|contributor| contributor.tools(&root_context))
|
||||
.collect::<Vec<_>>();
|
||||
let tools_without_memories = registry
|
||||
.mcp_tool_contributors()
|
||||
.tool_contributors()
|
||||
.iter()
|
||||
.flat_map(|contributor| contributor.tools(&memories_disabled_context))
|
||||
.collect::<Vec<_>>();
|
||||
let active_approval_interceptors = registry
|
||||
.approval_interceptor_contributors()
|
||||
.iter()
|
||||
.filter(|contributor| contributor.intercepts_approvals(&root_context))
|
||||
.count();
|
||||
|
||||
println!("prompt fragments: {}", prompt_fragments.len());
|
||||
println!("mcp tools: {}", tools.len());
|
||||
println!("approval interceptors: {active_approval_interceptors}");
|
||||
println!("native tools: {}", tools.len());
|
||||
println!(
|
||||
"mcp tools when use_memories=false: {}",
|
||||
"native tools when use_memories=false: {}",
|
||||
tools_without_memories.len()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
mod ctx {
|
||||
use codex_git_attribution::GitAttributionContext;
|
||||
use codex_guardian::GuardianContext;
|
||||
use codex_memories::MemoriesContext;
|
||||
use codex_multi_agent_v2::{MultiAgentV2Context, UsageHintAudience};
|
||||
use codex_multi_agent_v2::MultiAgentV2Context;
|
||||
use codex_multi_agent_v2::UsageHintAudience;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeContext {
|
||||
pub automatic_review_enabled: bool,
|
||||
pub approval_policy_allows_automatic_review: bool,
|
||||
pub is_guardian_reviewer: bool,
|
||||
pub guardian_policy_prompt: Option<String>,
|
||||
// Ideally this should be at the config layer instead
|
||||
pub commit_attribution: Option<String>,
|
||||
pub memory_tool_enabled: bool,
|
||||
@@ -77,6 +91,24 @@ mod ctx {
|
||||
pub subagent_usage_hint_text: Option<String>,
|
||||
}
|
||||
|
||||
impl GuardianContext for RuntimeContext {
|
||||
fn automatic_review_enabled(&self) -> bool {
|
||||
self.automatic_review_enabled
|
||||
}
|
||||
|
||||
fn approval_policy_allows_automatic_review(&self) -> bool {
|
||||
self.approval_policy_allows_automatic_review
|
||||
}
|
||||
|
||||
fn is_guardian_reviewer(&self) -> bool {
|
||||
self.is_guardian_reviewer
|
||||
}
|
||||
|
||||
fn guardian_policy_prompt(&self) -> Option<&str> {
|
||||
self.guardian_policy_prompt.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl GitAttributionContext for RuntimeContext {
|
||||
fn commit_attribution(&self) -> Option<&str> {
|
||||
self.commit_attribution.as_deref()
|
||||
@@ -110,4 +142,4 @@ mod ctx {
|
||||
self.subagent_usage_hint_text.as_deref()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,23 @@
|
||||
//! supporting types nearby.
|
||||
|
||||
mod prompt;
|
||||
mod tool;
|
||||
|
||||
pub use prompt::PromptFragment;
|
||||
pub use prompt::PromptSlot;
|
||||
pub use tool::ToolCallError;
|
||||
pub use tool::ToolContribution;
|
||||
pub use tool::ToolHandler;
|
||||
|
||||
use rmcp::model::Tool;
|
||||
/// Extension contribution that can claim approval requests for a runtime context.
|
||||
///
|
||||
/// Implementations should make only the routing decision here. The host keeps
|
||||
/// ownership of executing the chosen review flow and translating its result
|
||||
/// back into the surrounding runtime.
|
||||
pub trait ApprovalInterceptorContributor<C>: Send + Sync {
|
||||
/// Returns whether this contributor should intercept approvals in `context`.
|
||||
fn intercepts_approvals(&self, context: &C) -> bool;
|
||||
}
|
||||
|
||||
/// Extension contribution that adds prompt fragments during prompt assembly.
|
||||
///
|
||||
@@ -22,15 +34,13 @@ pub trait PromptContributor<C>: Send + Sync {
|
||||
fn contribute(&self, context: &C) -> Vec<PromptFragment>;
|
||||
}
|
||||
|
||||
/// Extension contribution that exposes MCP tool definitions owned by a feature.
|
||||
/// Extension contribution that exposes native tools 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>;
|
||||
/// The host remains responsible for mounting those tools and adapting calls
|
||||
/// into its runtime.
|
||||
pub trait ToolContributor<C>: Send + Sync {
|
||||
/// Returns the native tools visible for the supplied runtime context.
|
||||
fn tools(&self, context: &C) -> Vec<ToolContribution<C>>;
|
||||
}
|
||||
|
||||
71
codex-rs/ext/extension-api/src/contributors/tool.rs
Normal file
71
codex-rs/ext/extension-api/src/contributors/tool.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use serde_json::Value;
|
||||
use thiserror::Error;
|
||||
|
||||
// TMP
|
||||
#[derive(Clone)]
|
||||
pub struct ToolContribution<C> {
|
||||
spec: ResponsesApiTool,
|
||||
handler: Arc<dyn ToolHandler<C>>,
|
||||
supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
impl<C> ToolContribution<C> {
|
||||
pub fn new(spec: ResponsesApiTool, handler: Arc<dyn ToolHandler<C>>) -> Self {
|
||||
Self {
|
||||
spec,
|
||||
handler,
|
||||
supports_parallel_tool_calls: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn allow_parallel_calls(mut self) -> Self {
|
||||
self.supports_parallel_tool_calls = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn spec(&self) -> &ResponsesApiTool {
|
||||
&self.spec
|
||||
}
|
||||
|
||||
pub fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.supports_parallel_tool_calls
|
||||
}
|
||||
|
||||
pub fn handler(&self) -> Arc<dyn ToolHandler<C>> {
|
||||
Arc::clone(&self.handler)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////// Just to make it compile ////////////////////////////////
|
||||
pub trait ToolHandler<C>: Send + Sync {
|
||||
/// Handles one JSON-encoded invocation for this tool.
|
||||
fn handle<'a>(
|
||||
&'a self,
|
||||
context: &'a C,
|
||||
arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + Send + 'a>>;
|
||||
}
|
||||
|
||||
/// Error returned by a contributed native tool handler.
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq)]
|
||||
#[error("{message}")]
|
||||
pub struct ToolCallError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl ToolCallError {
|
||||
/// Creates a contributed-tool error with the supplied model-visible text.
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,14 @@ mod contributors;
|
||||
mod extension;
|
||||
mod registry;
|
||||
|
||||
pub use contributors::McpToolContributor;
|
||||
pub use contributors::ApprovalInterceptorContributor;
|
||||
pub use contributors::PromptContributor;
|
||||
pub use contributors::PromptFragment;
|
||||
pub use contributors::PromptSlot;
|
||||
pub use contributors::ToolCallError;
|
||||
pub use contributors::ToolContribution;
|
||||
pub use contributors::ToolContributor;
|
||||
pub use contributors::ToolHandler;
|
||||
pub use extension::CodexExtension;
|
||||
pub use registry::ExtensionRegistry;
|
||||
pub use registry::ExtensionRegistryBuilder;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::ApprovalInterceptorContributor;
|
||||
use crate::CodexExtension;
|
||||
use crate::McpToolContributor;
|
||||
use crate::PromptContributor;
|
||||
use crate::ToolContributor;
|
||||
|
||||
/// Mutable registry used while extensions install their typed contributions.
|
||||
pub struct ExtensionRegistryBuilder<C> {
|
||||
mcp_tool_contributors: Vec<Arc<dyn McpToolContributor<C>>>,
|
||||
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor<C>>>,
|
||||
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor<C>>>,
|
||||
}
|
||||
|
||||
impl<C> Default for ExtensionRegistryBuilder<C> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mcp_tool_contributors: Vec::new(),
|
||||
approval_interceptor_contributors: Vec::new(),
|
||||
prompt_contributors: Vec::new(),
|
||||
tool_contributors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,9 +46,12 @@ impl<C> ExtensionRegistryBuilder<C> {
|
||||
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 approval interceptor contributor.
|
||||
pub fn approval_interceptor_contributor(
|
||||
&mut self,
|
||||
contributor: Arc<dyn ApprovalInterceptorContributor<C>>,
|
||||
) {
|
||||
self.approval_interceptor_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one prompt contributor.
|
||||
@@ -53,29 +59,43 @@ impl<C> ExtensionRegistryBuilder<C> {
|
||||
self.prompt_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Registers one native tool contributor.
|
||||
pub fn tool_contributor(&mut self, contributor: Arc<dyn ToolContributor<C>>) {
|
||||
self.tool_contributors.push(contributor);
|
||||
}
|
||||
|
||||
/// Finishes construction and returns the immutable registry.
|
||||
pub fn build(self) -> ExtensionRegistry<C> {
|
||||
ExtensionRegistry {
|
||||
mcp_tool_contributors: self.mcp_tool_contributors,
|
||||
approval_interceptor_contributors: self.approval_interceptor_contributors,
|
||||
prompt_contributors: self.prompt_contributors,
|
||||
tool_contributors: self.tool_contributors,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable typed registry produced after extensions are installed.
|
||||
pub struct ExtensionRegistry<C> {
|
||||
mcp_tool_contributors: Vec<Arc<dyn McpToolContributor<C>>>,
|
||||
approval_interceptor_contributors: Vec<Arc<dyn ApprovalInterceptorContributor<C>>>,
|
||||
prompt_contributors: Vec<Arc<dyn PromptContributor<C>>>,
|
||||
tool_contributors: Vec<Arc<dyn ToolContributor<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 approval interceptor contributors.
|
||||
pub fn approval_interceptor_contributors(
|
||||
&self,
|
||||
) -> &[Arc<dyn ApprovalInterceptorContributor<C>>] {
|
||||
&self.approval_interceptor_contributors
|
||||
}
|
||||
|
||||
/// Returns the registered prompt contributors.
|
||||
pub fn prompt_contributors(&self) -> &[Arc<dyn PromptContributor<C>>] {
|
||||
&self.prompt_contributors
|
||||
}
|
||||
|
||||
/// Returns the registered native tool contributors.
|
||||
pub fn tool_contributors(&self) -> &[Arc<dyn ToolContributor<C>>] {
|
||||
&self.tool_contributors
|
||||
}
|
||||
}
|
||||
|
||||
6
codex-rs/ext/guardian/BUILD.bazel
Normal file
6
codex-rs/ext/guardian/BUILD.bazel
Normal file
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "guardian",
|
||||
crate_name = "codex_guardian",
|
||||
)
|
||||
15
codex-rs/ext/guardian/Cargo.toml
Normal file
15
codex-rs/ext/guardian/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-guardian"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_guardian"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-extension-api = { workspace = true }
|
||||
75
codex-rs/ext/guardian/src/lib.rs
Normal file
75
codex-rs/ext/guardian/src/lib.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Guardian routing contribution packaged as a Codex extension.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_extension_api::ApprovalInterceptorContributor;
|
||||
use codex_extension_api::CodexExtension;
|
||||
use codex_extension_api::ExtensionRegistryBuilder;
|
||||
use codex_extension_api::PromptContributor;
|
||||
use codex_extension_api::PromptFragment;
|
||||
|
||||
/// Runtime facts needed to expose Guardian surfaces.
|
||||
///
|
||||
/// Hosts should provide the effective approval settings for the current turn,
|
||||
/// whether the current session is the Guardian reviewer itself, and the prompt
|
||||
/// text to show to that reviewer.
|
||||
pub trait GuardianContext {
|
||||
fn automatic_review_enabled(&self) -> bool;
|
||||
fn approval_policy_allows_automatic_review(&self) -> bool;
|
||||
fn is_guardian_reviewer(&self) -> bool;
|
||||
fn guardian_policy_prompt(&self) -> Option<&str>;
|
||||
}
|
||||
|
||||
/// Extension that contributes Guardian approval routing and reviewer policy.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct GuardianExtension;
|
||||
|
||||
impl GuardianExtension {
|
||||
/// Creates an extension instance.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Returns whether Guardian should intercept approvals in this context.
|
||||
pub fn should_intercept_approvals<C: GuardianContext>(&self, context: &C) -> bool {
|
||||
context.automatic_review_enabled() && context.approval_policy_allows_automatic_review()
|
||||
}
|
||||
|
||||
/// Returns the policy prompt shown only to Guardian reviewer sessions.
|
||||
pub fn policy_prompt<'a, C: GuardianContext>(&self, context: &'a C) -> Option<&'a str> {
|
||||
if context.is_guardian_reviewer() {
|
||||
context.guardian_policy_prompt()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: GuardianContext> ApprovalInterceptorContributor<C> for GuardianExtension {
|
||||
fn intercepts_approvals(&self, context: &C) -> bool {
|
||||
self.should_intercept_approvals(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: GuardianContext> PromptContributor<C> for GuardianExtension {
|
||||
fn contribute(&self, context: &C) -> Vec<PromptFragment> {
|
||||
self.policy_prompt(context)
|
||||
.map(PromptFragment::separate_developer)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: GuardianContext> CodexExtension<C> for GuardianExtension {
|
||||
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
|
||||
registry.approval_interceptor_contributor(self.clone());
|
||||
registry.prompt_contributor(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a shared Guardian extension instance.
|
||||
pub fn extension() -> Arc<GuardianExtension> {
|
||||
Arc::new(GuardianExtension::new())
|
||||
}
|
||||
@@ -14,9 +14,9 @@ workspace = true
|
||||
[dependencies]
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-memories-read = { workspace = true }
|
||||
codex-tools = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod list_tool;
|
||||
|
||||
use std::path::PathBuf;
|
||||
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_extension_api::ToolContribution;
|
||||
use codex_extension_api::ToolContributor;
|
||||
use codex_memories_read::build_memory_tool_developer_instructions;
|
||||
use codex_memories_read::memory_root;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use rmcp::model::Tool;
|
||||
use rmcp::model::ToolAnnotations;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use list_tool::ListMemoriesTool;
|
||||
|
||||
/// Runtime facts needed to decide whether read-memory surfaces are visible.
|
||||
///
|
||||
@@ -25,23 +27,37 @@ pub trait MemoriesContext {
|
||||
fn use_memories(&self) -> bool;
|
||||
}
|
||||
|
||||
/// Extension that contributes memories MCP tools plus their read-path guidance.
|
||||
/// Extension that contributes memories read surfaces.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MemoriesExtension {
|
||||
read_prompt: Option<String>,
|
||||
list_tool: Arc<ListMemoriesTool>,
|
||||
}
|
||||
|
||||
impl MemoriesExtension {
|
||||
/// Creates an extension from a pre-rendered read prompt.
|
||||
pub fn new(read_prompt: Option<String>) -> Self {
|
||||
Self { read_prompt }
|
||||
fn new(read_prompt: Option<String>, memories_root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
read_prompt,
|
||||
list_tool: Arc::new(ListMemoriesTool::new(memories_root)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an extension that contributes native tools but no prompt fragment.
|
||||
pub fn tools_only(memories_root: impl Into<PathBuf>) -> Self {
|
||||
Self::new(None, memories_root)
|
||||
}
|
||||
|
||||
/// Creates an extension with one pre-rendered read prompt and native tools.
|
||||
pub fn with_read_prompt(read_prompt: String, memories_root: impl Into<PathBuf>) -> Self {
|
||||
Self::new(Some(read_prompt), memories_root)
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
Self::new(
|
||||
build_memory_tool_developer_instructions(codex_home).await,
|
||||
memory_root(codex_home).to_path_buf(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the rendered developer instruction for read access, if available.
|
||||
@@ -54,17 +70,13 @@ impl MemoriesExtension {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MemoriesContext> McpToolContributor<C> for MemoriesExtension {
|
||||
fn tools(&self, context: &C) -> Vec<Tool> {
|
||||
impl<C: MemoriesContext + Send + Sync + 'static> ToolContributor<C> for MemoriesExtension {
|
||||
fn tools(&self, context: &C) -> Vec<ToolContribution<C>> {
|
||||
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."),
|
||||
]
|
||||
vec![self.list_tool.contribution()]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +93,9 @@ impl<C: MemoriesContext> PromptContributor<C> for MemoriesExtension {
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: MemoriesContext> CodexExtension<C> for MemoriesExtension {
|
||||
impl<C: MemoriesContext + Send + Sync + 'static> CodexExtension<C> for MemoriesExtension {
|
||||
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<C>) {
|
||||
registry.mcp_tool_contributor(self.clone());
|
||||
registry.tool_contributor(self.clone());
|
||||
registry.prompt_contributor(self);
|
||||
}
|
||||
}
|
||||
@@ -92,12 +104,3 @@ impl<C: MemoriesContext> CodexExtension<C> for MemoriesExtension {
|
||||
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()))])
|
||||
}
|
||||
|
||||
321
codex-rs/ext/memories/src/list_tool.rs
Normal file
321
codex-rs/ext/memories/src/list_tool.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
use std::borrow::Cow;
|
||||
use std::future::Future;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_extension_api::ToolCallError;
|
||||
use codex_extension_api::ToolContribution;
|
||||
use codex_extension_api::ToolHandler;
|
||||
use codex_tools::JsonSchema;
|
||||
use codex_tools::ResponsesApiTool;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
const LIST_MEMORIES_TOOL_NAME: &str = "list_memories";
|
||||
const DEFAULT_LIST_MAX_RESULTS: usize = 2_000;
|
||||
const MAX_LIST_RESULTS: usize = 2_000;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ListMemoriesTool {
|
||||
memories_root: PathBuf,
|
||||
}
|
||||
|
||||
impl ListMemoriesTool {
|
||||
pub(super) fn new(memories_root: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
memories_root: memories_root.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn contribution<C>(self: &Arc<Self>) -> ToolContribution<C>
|
||||
where
|
||||
C: Send + Sync + 'static,
|
||||
{
|
||||
let handler: Arc<dyn ToolHandler<C>> = self.clone();
|
||||
ToolContribution::new(create_list_memories_tool(), handler).allow_parallel_calls()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ToolHandler<C> for ListMemoriesTool
|
||||
where
|
||||
C: Send + Sync,
|
||||
{
|
||||
fn handle<'a>(
|
||||
&'a self,
|
||||
_context: &'a C,
|
||||
arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let args: ListMemoriesArgs = serde_json::from_value(arguments)
|
||||
.map_err(|err| ToolCallError::new(format!("invalid arguments: {err}")))?;
|
||||
tokio::fs::create_dir_all(&self.memories_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
ToolCallError::new(format!(
|
||||
"failed to create memories root at {}: {err}",
|
||||
self.memories_root.display()
|
||||
))
|
||||
})?;
|
||||
let response = list_memories(&self.memories_root, args)
|
||||
.await
|
||||
.map_err(|err| ToolCallError::new(err.to_string()))?;
|
||||
serde_json::to_value(response)
|
||||
.map_err(|err| ToolCallError::new(format!("failed to serialize output: {err}")))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct ListMemoriesArgs {
|
||||
path: Option<String>,
|
||||
cursor: Option<String>,
|
||||
max_results: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct ListMemoriesResponse {
|
||||
path: Option<String>,
|
||||
entries: Vec<MemoryEntry>,
|
||||
next_cursor: Option<String>,
|
||||
truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct MemoryEntry {
|
||||
path: String,
|
||||
entry_type: MemoryEntryType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum MemoryEntryType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum ListMemoriesError {
|
||||
#[error("path '{path}' {reason}")]
|
||||
InvalidPath { path: String, reason: String },
|
||||
#[error("cursor '{cursor}' {reason}")]
|
||||
InvalidCursor { cursor: String, reason: String },
|
||||
#[error("path '{path}' was not found")]
|
||||
NotFound { path: String },
|
||||
#[error("I/O error while reading memories: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
async fn list_memories(
|
||||
memories_root: &Path,
|
||||
args: ListMemoriesArgs,
|
||||
) -> Result<ListMemoriesResponse, ListMemoriesError> {
|
||||
let max_results = args
|
||||
.max_results
|
||||
.unwrap_or(DEFAULT_LIST_MAX_RESULTS)
|
||||
.min(MAX_LIST_RESULTS);
|
||||
let start = resolve_scoped_path(memories_root, args.path.as_deref()).await?;
|
||||
let start_index = match args.cursor.as_deref() {
|
||||
Some(cursor) => cursor
|
||||
.parse::<usize>()
|
||||
.map_err(|_| ListMemoriesError::InvalidCursor {
|
||||
cursor: cursor.to_string(),
|
||||
reason: "must be a non-negative integer".to_string(),
|
||||
})?,
|
||||
None => 0,
|
||||
};
|
||||
let Some(metadata) = metadata_or_none(&start).await? else {
|
||||
return Err(ListMemoriesError::NotFound {
|
||||
path: args.path.unwrap_or_default(),
|
||||
});
|
||||
};
|
||||
reject_symlink(&display_relative_path(memories_root, &start), &metadata)?;
|
||||
|
||||
let mut entries = if metadata.is_file() {
|
||||
vec![MemoryEntry {
|
||||
path: display_relative_path(memories_root, &start),
|
||||
entry_type: MemoryEntryType::File,
|
||||
}]
|
||||
} else if metadata.is_dir() {
|
||||
let mut entries = Vec::new();
|
||||
for path in read_sorted_dir_paths(&start).await? {
|
||||
if is_hidden_path(&path) {
|
||||
continue;
|
||||
}
|
||||
let Some(metadata) = metadata_or_none(&path).await? else {
|
||||
continue;
|
||||
};
|
||||
if metadata.file_type().is_symlink() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry_type = if metadata.is_dir() {
|
||||
MemoryEntryType::Directory
|
||||
} else if metadata.is_file() {
|
||||
MemoryEntryType::File
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
entries.push(MemoryEntry {
|
||||
path: display_relative_path(memories_root, &path),
|
||||
entry_type,
|
||||
});
|
||||
}
|
||||
entries
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if start_index > entries.len() {
|
||||
return Err(ListMemoriesError::InvalidCursor {
|
||||
cursor: start_index.to_string(),
|
||||
reason: "exceeds result count".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let end_index = start_index.saturating_add(max_results).min(entries.len());
|
||||
let next_cursor = (end_index < entries.len()).then(|| end_index.to_string());
|
||||
let truncated = next_cursor.is_some();
|
||||
Ok(ListMemoriesResponse {
|
||||
path: args.path,
|
||||
entries: entries.drain(start_index..end_index).collect(),
|
||||
next_cursor,
|
||||
truncated,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_scoped_path(
|
||||
memories_root: &Path,
|
||||
relative_path: Option<&str>,
|
||||
) -> Result<PathBuf, ListMemoriesError> {
|
||||
let Some(relative_path) = relative_path else {
|
||||
return Ok(memories_root.to_path_buf());
|
||||
};
|
||||
let relative = Path::new(relative_path);
|
||||
if relative.components().any(|component| {
|
||||
matches!(
|
||||
component,
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
||||
)
|
||||
}) {
|
||||
return Err(ListMemoriesError::InvalidPath {
|
||||
path: relative_path.to_string(),
|
||||
reason: "must stay within the memories root".to_string(),
|
||||
});
|
||||
}
|
||||
if relative.components().any(is_hidden_component) {
|
||||
return Err(ListMemoriesError::NotFound {
|
||||
path: relative_path.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let components = relative.components().collect::<Vec<_>>();
|
||||
let mut scoped_path = memories_root.to_path_buf();
|
||||
for (index, component) in components.iter().enumerate() {
|
||||
scoped_path.push(component.as_os_str());
|
||||
|
||||
let Some(metadata) = metadata_or_none(&scoped_path).await? else {
|
||||
for remaining_component in components.iter().skip(index + 1) {
|
||||
scoped_path.push(remaining_component.as_os_str());
|
||||
}
|
||||
return Ok(scoped_path);
|
||||
};
|
||||
|
||||
reject_symlink(
|
||||
&display_relative_path(memories_root, &scoped_path),
|
||||
&metadata,
|
||||
)?;
|
||||
if index + 1 < components.len() && !metadata.is_dir() {
|
||||
return Err(ListMemoriesError::InvalidPath {
|
||||
path: relative_path.to_string(),
|
||||
reason: "traverses through a non-directory path component".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scoped_path)
|
||||
}
|
||||
|
||||
async fn metadata_or_none(path: &Path) -> Result<Option<std::fs::Metadata>, ListMemoriesError> {
|
||||
match tokio::fs::symlink_metadata(path).await {
|
||||
Ok(metadata) => Ok(Some(metadata)),
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn reject_symlink(
|
||||
relative_path: &str,
|
||||
metadata: &std::fs::Metadata,
|
||||
) -> Result<(), ListMemoriesError> {
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err(ListMemoriesError::InvalidPath {
|
||||
path: relative_path.to_string(),
|
||||
reason: "must not be a symlink".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_relative_path(root: &Path, path: &Path) -> String {
|
||||
path.strip_prefix(root)
|
||||
.map_or(Cow::Borrowed(path), Cow::Borrowed)
|
||||
.to_string_lossy()
|
||||
.replace(std::path::MAIN_SEPARATOR, "/")
|
||||
}
|
||||
|
||||
fn is_hidden_path(path: &Path) -> bool {
|
||||
path.file_name()
|
||||
.is_some_and(|name| name.to_string_lossy().starts_with('.'))
|
||||
}
|
||||
|
||||
fn is_hidden_component(component: Component<'_>) -> bool {
|
||||
matches!(component, Component::Normal(name) if name.to_string_lossy().starts_with('.'))
|
||||
}
|
||||
|
||||
async fn read_sorted_dir_paths(path: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
|
||||
let mut entries = tokio::fs::read_dir(path).await?;
|
||||
let mut paths = Vec::new();
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
paths.push(entry.path());
|
||||
}
|
||||
paths.sort();
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
fn create_list_memories_tool() -> ResponsesApiTool {
|
||||
let properties = std::collections::BTreeMap::from([
|
||||
(
|
||||
"path".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Optional relative path to list inside the Codex memories store.".to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"cursor".to_string(),
|
||||
JsonSchema::string(Some(
|
||||
"Optional cursor returned by a previous list_memories call.".to_string(),
|
||||
)),
|
||||
),
|
||||
(
|
||||
"max_results".to_string(),
|
||||
JsonSchema::integer(Some(
|
||||
"Optional maximum number of entries to return.".to_string(),
|
||||
)),
|
||||
),
|
||||
]);
|
||||
|
||||
ResponsesApiTool {
|
||||
name: LIST_MEMORIES_TOOL_NAME.to_string(),
|
||||
description:
|
||||
"List immediate files and directories under a path in the Codex memories store."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())),
|
||||
output_schema: None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user