Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
46b6cd3
feat: implement multi-agent orchestration system
metaphorics Sep 15, 2025
9582502
Merge branch 'openai:main' into main
metaphorics Sep 15, 2025
160774d
chore: apply clippy lints
metaphorics Sep 15, 2025
f3d900f
Merge branch 'main' into main
metaphorics Sep 15, 2025
26975dc
Merge branch 'main' into main
metaphorics Sep 15, 2025
ae07076
feat: update subagents ux experiences
metaphorics Sep 15, 2025
f3f14bd
docs: update documents to meet the new subagents features accordingly
metaphorics Sep 15, 2025
2c2768a
Merge branch 'main' into main
metaphorics Sep 15, 2025
458e3d1
feat: implement path validation for agent prompt files
metaphorics Sep 15, 2025
2c322f2
refactor: enhance agent context handling and task extraction
metaphorics Sep 15, 2025
117f524
chore: update tests accordingly (added agent tool)
metaphorics Sep 15, 2025
cfd8248
Merge branch 'main' into main
metaphorics Sep 15, 2025
54b6acd
refactor: improve agent ID generation and context management
metaphorics Sep 15, 2025
23d04b1
refactor: improve agent context management and logging
metaphorics Sep 15, 2025
cd24d73
Merge branch 'main' into main
metaphorics Sep 15, 2025
275f92e
refactor: enhance agent event handling and context management
metaphorics Sep 15, 2025
d7f89b1
Merge branch 'main' into main
metaphorics Sep 15, 2025
784464e
Implement validation and prompt loading for AgentConfig
metaphorics Sep 15, 2025
374dcf7
Refactor agent tool call handling for parallel execution
metaphorics Sep 16, 2025
1ad8bae
Merge branch 'main' into main
metaphorics Sep 16, 2025
7199be9
fix: Refactor agent execution for true parallelism
metaphorics Sep 16, 2025
6a36f32
refactor: Improve mutex handling and state management
metaphorics Sep 16, 2025
96aaa91
refactor: Improve tool call execution strategy for concurrency
metaphorics Sep 16, 2025
0af39e8
Merge branch 'main' into main
metaphorics Sep 16, 2025
9f4d05d
fixr: Update agent message structure for input handling
metaphorics Sep 16, 2025
8d526a3
refactor: Enhance agent tool handling for improved parallel execution
metaphorics Sep 16, 2025
086a049
refactor: Implement agent context management to prevent recursion
metaphorics Sep 16, 2025
9f1e3c0
Merge branch 'main' into main
metaphorics Sep 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ You can also use Codex with an API key, but this requires [additional setup](./d

Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `~/.codex/config.toml`.


### Configuration

Codex CLI supports a rich set of configuration options, with preferences stored in `~/.codex/config.toml`. For full configuration options, see [Configuration](./docs/config.md).
Expand All @@ -88,6 +87,10 @@ Codex CLI supports a rich set of configuration options, with preferences stored
- [Non-interactive / CI mode](./docs/advanced.md#non-interactive--ci-mode)
- [Tracing / verbose logging](./docs/advanced.md#tracing--verbose-logging)
- [Model Context Protocol (MCP)](./docs/advanced.md#model-context-protocol-mcp)
- [**Multi-Agent System**](./docs/subagents.md)
- [Custom agent configuration](./docs/subagents.md#custom-agent-configuration)
- [Agent behavior](./docs/subagents.md#agent-behavior)
- [Best practices](./docs/subagents.md#best-practices)
- [**Zero data retention (ZDR)**](./docs/zdr.md)
- [**Contributing**](./docs/contributing.md)
- [**Install & build**](./docs/install.md)
Expand All @@ -102,4 +105,3 @@ Codex CLI supports a rich set of configuration options, with preferences stored
## License

This repository is licensed under the [Apache-2.0 License](LICENSE).

300 changes: 300 additions & 0 deletions codex-rs/core/src/agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
//! Multi-agent orchestration system with customizable system prompts
//!
//! This module provides a lightweight agent system where agents are primarily
//! specialized through custom system prompts while inheriting tools and permissions
//! from the current workspace context.

use crate::error::Result;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;

/// Configuration for a single agent
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
/// The system prompt that defines the agent's behavior
pub prompt: String,

/// Optional: Load prompt from file instead of inline
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_file: Option<String>,

/// Optional: Override tools (usually inherits from context)
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<Vec<String>>,

/// Optional: Override permissions (usually inherits from context)
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<String>,
}
Comment thread
metaphorics marked this conversation as resolved.

/// Registry of available agents and their configurations
pub struct AgentRegistry {
agents: HashMap<String, AgentConfig>,
#[allow(dead_code)]
agents_dir: Option<PathBuf>,
}

impl AgentRegistry {
/// Validate that a prompt file path doesn't escape allowed directories
fn validate_prompt_path(base_dir: &Path, prompt_file: &str) -> anyhow::Result<PathBuf> {
let path = if prompt_file.starts_with('/') {
PathBuf::from(prompt_file)
} else {
base_dir.join(prompt_file)
};

// Canonicalize to resolve ../ and symlinks
let canonical = path
.canonicalize()
.map_err(|e| anyhow::anyhow!("Cannot access prompt file: {}", e))?;

// Get the home/.codex directory
let home_codex = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Cannot determine home directory"))?
.join(".codex");

// Security check: path must be within ~/.codex or the base directory
if !canonical.starts_with(&home_codex) && !canonical.starts_with(base_dir) {
return Err(anyhow::anyhow!(
"Security error: Prompt file must be within ~/.codex directory"
));
}

Ok(canonical)
}

/// Create a new agent registry, loading user configurations if available
pub fn new() -> Result<Self> {
let mut agents = HashMap::new();

// Add the single default "general" agent
agents.insert(
"general".to_string(),
AgentConfig {
prompt: "You are a helpful AI assistant. Complete the given task efficiently and accurately.".to_string(),
prompt_file: None,
tools: None,
permissions: None,
}
);

// Try to load user agents from ~/.codex/agents.toml
let agents_dir = Self::get_agents_directory();
if let Some(ref dir) = agents_dir {
let config_path = dir.join("agents.toml");
if config_path.exists() {
match std::fs::read_to_string(&config_path) {
Ok(content) => {
match toml::from_str::<HashMap<String, AgentConfig>>(&content) {
Ok(user_agents) => {
// Process each agent config
for (name, mut config) in user_agents {
// If prompt_file is specified, load the prompt from file
if let Some(ref prompt_file) = config.prompt_file {
// Validate the path to prevent traversal attacks
match Self::validate_prompt_path(dir, prompt_file) {
Ok(safe_path) => {
match std::fs::read_to_string(&safe_path) {
Ok(prompt_content) => {
config.prompt = prompt_content;
tracing::debug!(
"Loaded prompt file for agent '{}'",
name
);
}
Err(e) => {
tracing::error!(
"Cannot read prompt file '{}' for agent '{}': {}",
prompt_file,
name,
e
);
// Skip this agent but continue loading others
continue;
}
}
}
Err(e) => {
tracing::error!(
"Agent '{}' configuration error: {}",
name,
e
);
// Skip this agent but continue loading others
continue;
}
}
}

agents.insert(name, config);
}
tracing::info!("Loaded {} user-defined agents", agents.len() - 1);
}
Err(e) => {
tracing::warn!("Failed to parse agents.toml: {}", e);
}
}
}
Err(e) => {
tracing::debug!("Could not read agents.toml: {}", e);
}
}
}
}

Ok(Self { agents, agents_dir })
}

/// Get the agents directory path (~/.codex/agents)
fn get_agents_directory() -> Option<PathBuf> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.ok()
.map(|home| PathBuf::from(home).join(".codex"))
}

/// Get an agent configuration by name
#[allow(dead_code)]
pub fn get_agent(&self, name: &str) -> Option<&AgentConfig> {
self.agents.get(name)
}

/// Get the system prompt for an agent (falls back to "general" if not found)
pub fn get_system_prompt(&self, agent_name: &str) -> String {
self.agents
.get(agent_name)
.or_else(|| self.agents.get("general"))
.map(|config| config.prompt.clone())
.unwrap_or_else(|| "You are a helpful AI assistant.".to_string())
}

/// List all available agents
#[allow(dead_code)]
pub fn list_agents(&self) -> Vec<String> {
self.agents.keys().cloned().collect()
}

/// Get detailed information about all agents
pub fn list_agent_details(&self) -> Vec<crate::protocol::AgentInfo> {
let mut agents = Vec::new();

for (name, config) in &self.agents {
let description = self.extract_description(&config.prompt);
agents.push(crate::protocol::AgentInfo {
name: name.clone(),
description,
is_builtin: name == "general",
});
}

agents.sort_by(|a, b| {
// Built-in agents first, then alphabetical
match (a.is_builtin, b.is_builtin) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
}
});

agents
}

/// Extract brief description from prompt
fn extract_description(&self, prompt: &str) -> String {
// Take first line or first sentence as description
let first_line = prompt.lines().next().unwrap_or("");
let desc = if let Some(pos) = first_line.find('.') {
&first_line[..=pos]
} else {
first_line
};

// Clean up common prefixes
desc.trim_start_matches("You are a ")
.trim_start_matches("You are an ")
.trim_start_matches("You are ")
.trim()
.to_string()
}

/// Check if agents can spawn other agents (always false to prevent recursion)
#[allow(dead_code)]
pub fn can_spawn_agents(metadata: &HashMap<String, String>) -> bool {
!metadata.contains_key("is_agent")
}

/// Mark a context as being an agent context
#[allow(dead_code)]
pub fn mark_as_agent_context(metadata: &mut HashMap<String, String>) {
metadata.insert("is_agent".to_string(), "true".to_string());
}
}

/// Execute an agent with a specific task
#[allow(dead_code)]
pub async fn execute_agent_task(
agent_name: &str,
task: String,
registry: &AgentRegistry,
) -> Result<String> {
// Get the agent's system prompt
let system_prompt = registry.get_system_prompt(agent_name);

// Build the specialized prompt for this agent
let full_prompt = format!("{system_prompt}\n\nTask: {task}");

// Note: The actual execution will be handled by the parent context
// using the existing conversation infrastructure
Ok(full_prompt)
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;

#[test]
fn test_default_agent_exists() {
let registry = AgentRegistry::new().unwrap();
assert!(registry.get_agent("general").is_some());
}

#[test]
fn test_agent_recursion_prevention() {
let mut metadata = HashMap::new();
assert!(AgentRegistry::can_spawn_agents(&metadata));

AgentRegistry::mark_as_agent_context(&mut metadata);
assert!(!AgentRegistry::can_spawn_agents(&metadata));
}

#[test]
fn test_path_traversal_prevention() {
// Create a temporary directory structure
let temp_dir = TempDir::new().unwrap();
let base_dir = temp_dir.path();

// Create a safe file
let safe_dir = base_dir.join("prompts");
fs::create_dir(&safe_dir).unwrap();
let safe_file = safe_dir.join("test.txt");
fs::write(&safe_file, "safe content").unwrap();

// Test that normal paths work
let result = AgentRegistry::validate_prompt_path(base_dir, "prompts/test.txt");
assert!(result.is_ok());

// Test that path traversal is blocked
let result = AgentRegistry::validate_prompt_path(base_dir, "../../../etc/passwd");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Security error"));

// Test that absolute paths outside allowed dirs are blocked
let result = AgentRegistry::validate_prompt_path(base_dir, "/etc/passwd");
assert!(result.is_err());
}
}
Loading