mirror of
https://github.com/openai/codex.git
synced 2026-09-29 16:57:06 +08:00
Extract agent role loading into a dedicated crate (#40487)
## What changed - Add `codex-agent-roles` for agent role configuration types, parsing, discovery, validation, and layered loading. - Update `codex-core` to consume the new crate's public role configuration and parsing APIs. GitOrigin-RevId: 5a963a219a581b2848dae6fd071aa31a0b5ff22c
This commit is contained in:
Generated
+14
@@ -1915,6 +1915,19 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-agent-roles"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-config",
|
||||
"codex-file-system",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-path-uri",
|
||||
"serde",
|
||||
"toml 0.9.11+spec-1.1.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-analytics"
|
||||
version = "0.0.0"
|
||||
@@ -2747,6 +2760,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-agent-graph-store",
|
||||
"codex-agent-roles",
|
||||
"codex-analytics",
|
||||
"codex-api",
|
||||
"codex-app-server-protocol",
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"analytics",
|
||||
"agent-graph-store",
|
||||
"agent-identity",
|
||||
"agent-roles",
|
||||
"backend-client",
|
||||
"bwrap",
|
||||
"build-info",
|
||||
@@ -153,6 +154,7 @@ app_test_support = { path = "app-server/tests/common" }
|
||||
codex-analytics = { path = "analytics" }
|
||||
codex-agent-graph-store = { path = "agent-graph-store" }
|
||||
codex-agent-identity = { path = "agent-identity" }
|
||||
codex-agent-roles = { path = "agent-roles" }
|
||||
codex-ansi-escape = { path = "ansi-escape" }
|
||||
codex-api = { path = "codex-api" }
|
||||
codex-aws-auth = { path = "aws-auth" }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "agent-roles",
|
||||
crate_name = "codex_agent_roles",
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-agent-roles"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
name = "codex_agent_roles"
|
||||
path = "src/lib.rs"
|
||||
test = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-config = { workspace = true }
|
||||
codex-file-system = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
toml = { workspace = true, features = ["preserve_order"] }
|
||||
tracing = { workspace = true }
|
||||
@@ -0,0 +1,209 @@
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AgentRoleConfig {
|
||||
/// Human-facing role documentation used in spawn tool guidance.
|
||||
/// Required for loaded user-defined roles after deprecated/new metadata precedence resolves.
|
||||
pub description: Option<String>,
|
||||
/// Path to a role-specific config layer.
|
||||
pub config_file: Option<PathBuf>,
|
||||
/// Candidate nicknames for agents spawned with this role.
|
||||
pub nickname_candidates: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawAgentRoleFileToml {
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
nickname_candidates: Option<Vec<String>>,
|
||||
#[serde(flatten)]
|
||||
config: ConfigToml,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ResolvedAgentRoleFile {
|
||||
pub role_name: String,
|
||||
pub description: Option<String>,
|
||||
pub nickname_candidates: Option<Vec<String>>,
|
||||
pub config: TomlValue,
|
||||
}
|
||||
|
||||
pub fn parse_agent_role_file_contents(
|
||||
contents: &str,
|
||||
role_file_label: &Path,
|
||||
config_base_dir: &Path,
|
||||
role_name_hint: Option<&str>,
|
||||
) -> std::io::Result<ResolvedAgentRoleFile> {
|
||||
let role_file_toml: TomlValue = toml::from_str(contents).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"failed to parse agent role file at {}: {err}",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let _guard = AbsolutePathBufGuard::new(config_base_dir);
|
||||
let parsed: RawAgentRoleFileToml = role_file_toml.clone().try_into().map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"failed to deserialize agent role file at {}: {err}",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let description = normalize_agent_role_description(
|
||||
&format!("agent role file {}.description", role_file_label.display()),
|
||||
parsed.description.as_deref(),
|
||||
)?;
|
||||
validate_agent_role_file_developer_instructions(
|
||||
role_file_label,
|
||||
parsed.config.developer_instructions.as_deref(),
|
||||
role_name_hint.is_none(),
|
||||
)?;
|
||||
|
||||
let role_name = parsed
|
||||
.name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| role_name_hint.map(ToOwned::to_owned))
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {} must define a non-empty `name`",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let nickname_candidates = normalize_agent_role_nickname_candidates(
|
||||
&format!(
|
||||
"agent role file {}.nickname_candidates",
|
||||
role_file_label.display()
|
||||
),
|
||||
parsed.nickname_candidates.as_deref(),
|
||||
)?;
|
||||
|
||||
let mut config = role_file_toml;
|
||||
let Some(config_table) = config.as_table_mut() else {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"agent role file at {} must contain a TOML table",
|
||||
role_file_label.display()
|
||||
),
|
||||
));
|
||||
};
|
||||
config_table.remove("name");
|
||||
config_table.remove("description");
|
||||
config_table.remove("nickname_candidates");
|
||||
|
||||
Ok(ResolvedAgentRoleFile {
|
||||
role_name,
|
||||
description,
|
||||
nickname_candidates,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_agent_role_description(
|
||||
field_label: &str,
|
||||
description: Option<&str>,
|
||||
) -> std::io::Result<Option<String>> {
|
||||
match description.map(str::trim) {
|
||||
Some("") => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot be blank"),
|
||||
)),
|
||||
Some(description) => Ok(Some(description.to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_agent_role_file_developer_instructions(
|
||||
role_file_label: &Path,
|
||||
developer_instructions: Option<&str>,
|
||||
require_present: bool,
|
||||
) -> std::io::Result<()> {
|
||||
match developer_instructions.map(str::trim) {
|
||||
Some("") => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {}.developer_instructions cannot be blank",
|
||||
role_file_label.display()
|
||||
),
|
||||
)),
|
||||
Some(_) => Ok(()),
|
||||
None if require_present => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {} must define `developer_instructions`",
|
||||
role_file_label.display()
|
||||
),
|
||||
)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_agent_role_nickname_candidates(
|
||||
field_label: &str,
|
||||
nickname_candidates: Option<&[String]>,
|
||||
) -> std::io::Result<Option<Vec<String>>> {
|
||||
let Some(nickname_candidates) = nickname_candidates else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if nickname_candidates.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} must contain at least one name"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut normalized_candidates = Vec::with_capacity(nickname_candidates.len());
|
||||
let mut seen_candidates = BTreeSet::new();
|
||||
|
||||
for nickname in nickname_candidates {
|
||||
let normalized_nickname = nickname.trim();
|
||||
if normalized_nickname.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot contain blank names"),
|
||||
));
|
||||
}
|
||||
|
||||
if !seen_candidates.insert(normalized_nickname.to_owned()) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot contain duplicates"),
|
||||
));
|
||||
}
|
||||
|
||||
if !normalized_nickname
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '-' | '_'))
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"{field_label} may only contain ASCII letters, digits, spaces, hyphens, and underscores"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
normalized_candidates.push(normalized_nickname.to_owned());
|
||||
}
|
||||
|
||||
Ok(Some(normalized_candidates))
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
pub(crate) async fn collect_agent_role_files(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
dir: &AbsolutePathBuf,
|
||||
) -> io::Result<Vec<AbsolutePathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
let mut dirs = vec![dir.clone()];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
let dir_uri = PathUri::from_abs_path(&dir);
|
||||
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
|
||||
Ok(entries) => entries,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let path = dir.join(entry.file_name);
|
||||
if entry.is_directory {
|
||||
dirs.push(path);
|
||||
continue;
|
||||
}
|
||||
if entry.is_file
|
||||
&& path
|
||||
.as_path()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "toml")
|
||||
{
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod agent_role_config;
|
||||
mod discovery;
|
||||
mod loader;
|
||||
|
||||
pub use agent_role_config::AgentRoleConfig;
|
||||
pub use agent_role_config::ResolvedAgentRoleFile;
|
||||
pub use agent_role_config::parse_agent_role_file_contents;
|
||||
pub use loader::load_agent_roles;
|
||||
@@ -1,23 +1,26 @@
|
||||
use super::AgentRoleConfig;
|
||||
use crate::AgentRoleConfig;
|
||||
use crate::ResolvedAgentRoleFile;
|
||||
use crate::agent_role_config::normalize_agent_role_description;
|
||||
use crate::agent_role_config::normalize_agent_role_nickname_candidates;
|
||||
use crate::discovery::collect_agent_role_files;
|
||||
use crate::parse_agent_role_file_contents;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::config_toml::AgentRoleToml;
|
||||
use codex_config::config_toml::AgentsToml;
|
||||
use codex_config::config_toml::ConfigToml;
|
||||
use codex_exec_server::ExecutorFileSystem;
|
||||
use codex_exec_server::GetMetadataOptions;
|
||||
use codex_exec_server::ReadFileOptions;
|
||||
use codex_file_system::ExecutorFileSystem;
|
||||
use codex_file_system::GetMetadataOptions;
|
||||
use codex_file_system::ReadFileOptions;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use toml::Value as TomlValue;
|
||||
|
||||
pub(crate) async fn load_agent_roles(
|
||||
pub async fn load_agent_roles(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
cfg: &ConfigToml,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
@@ -213,106 +216,6 @@ async fn agent_role_config_from_toml(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RawAgentRoleFileToml {
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
nickname_candidates: Option<Vec<String>>,
|
||||
#[serde(flatten)]
|
||||
config: ConfigToml,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ResolvedAgentRoleFile {
|
||||
pub(crate) role_name: String,
|
||||
pub(crate) description: Option<String>,
|
||||
pub(crate) nickname_candidates: Option<Vec<String>>,
|
||||
pub(crate) config: TomlValue,
|
||||
}
|
||||
|
||||
pub(crate) fn parse_agent_role_file_contents(
|
||||
contents: &str,
|
||||
role_file_label: &Path,
|
||||
config_base_dir: &Path,
|
||||
role_name_hint: Option<&str>,
|
||||
) -> std::io::Result<ResolvedAgentRoleFile> {
|
||||
let role_file_toml: TomlValue = toml::from_str(contents).map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"failed to parse agent role file at {}: {err}",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let _guard = AbsolutePathBufGuard::new(config_base_dir);
|
||||
let parsed: RawAgentRoleFileToml = role_file_toml.clone().try_into().map_err(|err| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"failed to deserialize agent role file at {}: {err}",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let description = normalize_agent_role_description(
|
||||
&format!("agent role file {}.description", role_file_label.display()),
|
||||
parsed.description.as_deref(),
|
||||
)?;
|
||||
validate_agent_role_file_developer_instructions(
|
||||
role_file_label,
|
||||
parsed.config.developer_instructions.as_deref(),
|
||||
role_name_hint.is_none(),
|
||||
)?;
|
||||
|
||||
let role_name = parsed
|
||||
.name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| role_name_hint.map(ToOwned::to_owned))
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {} must define a non-empty `name`",
|
||||
role_file_label.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let nickname_candidates = normalize_agent_role_nickname_candidates(
|
||||
&format!(
|
||||
"agent role file {}.nickname_candidates",
|
||||
role_file_label.display()
|
||||
),
|
||||
parsed.nickname_candidates.as_deref(),
|
||||
)?;
|
||||
|
||||
let mut config = role_file_toml;
|
||||
let Some(config_table) = config.as_table_mut() else {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"agent role file at {} must contain a TOML table",
|
||||
role_file_label.display()
|
||||
),
|
||||
));
|
||||
};
|
||||
config_table.remove("name");
|
||||
config_table.remove("description");
|
||||
config_table.remove("nickname_candidates");
|
||||
|
||||
Ok(ResolvedAgentRoleFile {
|
||||
role_name,
|
||||
description,
|
||||
nickname_candidates,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_resolved_agent_role_file(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
path: &AbsolutePathBuf,
|
||||
@@ -331,20 +234,6 @@ async fn read_resolved_agent_role_file(
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_agent_role_description(
|
||||
field_label: &str,
|
||||
description: Option<&str>,
|
||||
) -> std::io::Result<Option<String>> {
|
||||
match description.map(str::trim) {
|
||||
Some("") => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot be blank"),
|
||||
)),
|
||||
Some(description) => Ok(Some(description.to_string())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_required_agent_role_description(
|
||||
role_name: &str,
|
||||
description: Option<&str>,
|
||||
@@ -359,31 +248,6 @@ fn validate_required_agent_role_description(
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_agent_role_file_developer_instructions(
|
||||
role_file_label: &Path,
|
||||
developer_instructions: Option<&str>,
|
||||
require_present: bool,
|
||||
) -> std::io::Result<()> {
|
||||
match developer_instructions.map(str::trim) {
|
||||
Some("") => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {}.developer_instructions cannot be blank",
|
||||
role_file_label.display()
|
||||
),
|
||||
)),
|
||||
Some(_) => Ok(()),
|
||||
None if require_present => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"agent role file at {} must define `developer_instructions`",
|
||||
role_file_label.display()
|
||||
),
|
||||
)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn validate_agent_role_config_file(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
role_name: &str,
|
||||
@@ -423,58 +287,6 @@ async fn validate_agent_role_config_file(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_agent_role_nickname_candidates(
|
||||
field_label: &str,
|
||||
nickname_candidates: Option<&[String]>,
|
||||
) -> std::io::Result<Option<Vec<String>>> {
|
||||
let Some(nickname_candidates) = nickname_candidates else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if nickname_candidates.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} must contain at least one name"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut normalized_candidates = Vec::with_capacity(nickname_candidates.len());
|
||||
let mut seen_candidates = BTreeSet::new();
|
||||
|
||||
for nickname in nickname_candidates {
|
||||
let normalized_nickname = nickname.trim();
|
||||
if normalized_nickname.is_empty() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot contain blank names"),
|
||||
));
|
||||
}
|
||||
|
||||
if !seen_candidates.insert(normalized_nickname.to_owned()) {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!("{field_label} cannot contain duplicates"),
|
||||
));
|
||||
}
|
||||
|
||||
if !normalized_nickname
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || matches!(c, ' ' | '-' | '_'))
|
||||
{
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"{field_label} may only contain ASCII letters, digits, spaces, hyphens, and underscores"
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
normalized_candidates.push(normalized_nickname.to_owned());
|
||||
}
|
||||
|
||||
Ok(Some(normalized_candidates))
|
||||
}
|
||||
|
||||
async fn discover_agent_roles_in_dir(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
agents_dir: &AbsolutePathBuf,
|
||||
@@ -521,38 +333,3 @@ async fn discover_agent_roles_in_dir(
|
||||
|
||||
Ok(roles)
|
||||
}
|
||||
|
||||
async fn collect_agent_role_files(
|
||||
fs: &dyn ExecutorFileSystem,
|
||||
dir: &AbsolutePathBuf,
|
||||
) -> std::io::Result<Vec<AbsolutePathBuf>> {
|
||||
let mut files = Vec::new();
|
||||
let mut dirs = vec![dir.clone()];
|
||||
while let Some(dir) = dirs.pop() {
|
||||
let dir_uri = PathUri::from_abs_path(&dir);
|
||||
let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await {
|
||||
Ok(entries) => entries,
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
let path = dir.join(entry.file_name);
|
||||
if entry.is_directory {
|
||||
dirs.push(path);
|
||||
continue;
|
||||
}
|
||||
if entry.is_file
|
||||
&& path
|
||||
.as_path()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "toml")
|
||||
{
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-agent-graph-store = { workspace = true }
|
||||
codex-agent-roles = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-apply-patch = { workspace = true }
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
use crate::config::AgentRoleConfig;
|
||||
use crate::config::Config;
|
||||
use crate::config::agent_roles::parse_agent_role_file_contents;
|
||||
use crate::config::deserialize_config_toml_with_base;
|
||||
use anyhow::anyhow;
|
||||
use codex_agent_roles::parse_agent_role_file_contents;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||
use codex_agent_roles::load_agent_roles;
|
||||
use codex_config::CloudConfigBundleLoader;
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
@@ -155,7 +156,6 @@ use codex_network_proxy::NetworkProxyConfig;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
pub(crate) mod agent_roles;
|
||||
mod auth_keyring;
|
||||
pub mod edit;
|
||||
mod managed_features;
|
||||
@@ -170,6 +170,7 @@ mod resolved_permission_profile;
|
||||
mod schema;
|
||||
pub use auth_keyring::bootstrap_auth_config;
|
||||
pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind;
|
||||
pub use codex_agent_roles::AgentRoleConfig;
|
||||
pub use codex_config::ConfigLoadOptions;
|
||||
pub use codex_config::Constrained;
|
||||
pub use codex_config::ConstraintError;
|
||||
@@ -2308,17 +2309,6 @@ pub fn set_default_oss_provider(codex_home: &Path, provider: &str) -> std::io::R
|
||||
.map_err(|err| std::io::Error::other(format!("failed to persist config.toml: {err}")))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct AgentRoleConfig {
|
||||
/// Human-facing role documentation used in spawn tool guidance.
|
||||
/// Required for loaded user-defined roles after deprecated/new metadata precedence resolves.
|
||||
pub description: Option<String>,
|
||||
/// Path to a role-specific config layer.
|
||||
pub config_file: Option<PathBuf>,
|
||||
/// Candidate nicknames for agents spawned with this role.
|
||||
pub nickname_candidates: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn resolve_tool_suggest_config(
|
||||
config_toml: &ConfigToml,
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
@@ -3676,8 +3666,7 @@ impl Config {
|
||||
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
|
||||
|
||||
let agent_roles =
|
||||
agent_roles::load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings)
|
||||
.await?;
|
||||
load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings).await?;
|
||||
|
||||
let openai_base_url = cfg
|
||||
.openai_base_url
|
||||
|
||||
Reference in New Issue
Block a user