mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00: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:
1 parent
49880081fe
commit
fb9311db5c
11 files changed
+317
-248
No files matched your search
Generated
+14
@@ -1915,6 +1915,19 @@ dependencies = [
|
|||||||
"sha2 0.10.9",
|
"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]]
|
[[package]]
|
||||||
name = "codex-analytics"
|
name = "codex-analytics"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@@ -2747,6 +2760,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"codex-agent-graph-store",
|
"codex-agent-graph-store",
|
||||||
|
"codex-agent-roles",
|
||||||
"codex-analytics",
|
"codex-analytics",
|
||||||
"codex-api",
|
"codex-api",
|
||||||
"codex-app-server-protocol",
|
"codex-app-server-protocol",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ members = [
|
|||||||
"analytics",
|
"analytics",
|
||||||
"agent-graph-store",
|
"agent-graph-store",
|
||||||
"agent-identity",
|
"agent-identity",
|
||||||
|
"agent-roles",
|
||||||
"backend-client",
|
"backend-client",
|
||||||
"bwrap",
|
"bwrap",
|
||||||
"build-info",
|
"build-info",
|
||||||
@@ -153,6 +154,7 @@ app_test_support = { path = "app-server/tests/common" }
|
|||||||
codex-analytics = { path = "analytics" }
|
codex-analytics = { path = "analytics" }
|
||||||
codex-agent-graph-store = { path = "agent-graph-store" }
|
codex-agent-graph-store = { path = "agent-graph-store" }
|
||||||
codex-agent-identity = { path = "agent-identity" }
|
codex-agent-identity = { path = "agent-identity" }
|
||||||
|
codex-agent-roles = { path = "agent-roles" }
|
||||||
codex-ansi-escape = { path = "ansi-escape" }
|
codex-ansi-escape = { path = "ansi-escape" }
|
||||||
codex-api = { path = "codex-api" }
|
codex-api = { path = "codex-api" }
|
||||||
codex-aws-auth = { path = "aws-auth" }
|
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::ConfigLayerStack;
|
||||||
use codex_config::config_toml::AgentRoleToml;
|
use codex_config::config_toml::AgentRoleToml;
|
||||||
use codex_config::config_toml::AgentsToml;
|
use codex_config::config_toml::AgentsToml;
|
||||||
use codex_config::config_toml::ConfigToml;
|
use codex_config::config_toml::ConfigToml;
|
||||||
use codex_exec_server::ExecutorFileSystem;
|
use codex_file_system::ExecutorFileSystem;
|
||||||
use codex_exec_server::GetMetadataOptions;
|
use codex_file_system::GetMetadataOptions;
|
||||||
use codex_exec_server::ReadFileOptions;
|
use codex_file_system::ReadFileOptions;
|
||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||||
use codex_utils_path_uri::PathUri;
|
use codex_utils_path_uri::PathUri;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::io::ErrorKind;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
|
|
||||||
pub(crate) async fn load_agent_roles(
|
pub async fn load_agent_roles(
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
cfg: &ConfigToml,
|
cfg: &ConfigToml,
|
||||||
config_layer_stack: &ConfigLayerStack,
|
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(
|
async fn read_resolved_agent_role_file(
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
path: &AbsolutePathBuf,
|
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(
|
fn validate_required_agent_role_description(
|
||||||
role_name: &str,
|
role_name: &str,
|
||||||
description: Option<&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(
|
async fn validate_agent_role_config_file(
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
role_name: &str,
|
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(
|
async fn discover_agent_roles_in_dir(
|
||||||
fs: &dyn ExecutorFileSystem,
|
fs: &dyn ExecutorFileSystem,
|
||||||
agents_dir: &AbsolutePathBuf,
|
agents_dir: &AbsolutePathBuf,
|
||||||
@@ -521,38 +333,3 @@ async fn discover_agent_roles_in_dir(
|
|||||||
|
|
||||||
Ok(roles)
|
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"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
codex-analytics = { workspace = true }
|
codex-analytics = { workspace = true }
|
||||||
codex-agent-graph-store = { workspace = true }
|
codex-agent-graph-store = { workspace = true }
|
||||||
|
codex-agent-roles = { workspace = true }
|
||||||
codex-api = { workspace = true }
|
codex-api = { workspace = true }
|
||||||
codex-app-server-protocol = { workspace = true }
|
codex-app-server-protocol = { workspace = true }
|
||||||
codex-apply-patch = { workspace = true }
|
codex-apply-patch = { workspace = true }
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
use crate::config::AgentRoleConfig;
|
use crate::config::AgentRoleConfig;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::config::agent_roles::parse_agent_role_file_contents;
|
|
||||||
use crate::config::deserialize_config_toml_with_base;
|
use crate::config::deserialize_config_toml_with_base;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
|
use codex_agent_roles::parse_agent_role_file_contents;
|
||||||
use codex_config::ConfigLayerEntry;
|
use codex_config::ConfigLayerEntry;
|
||||||
use codex_config::ConfigLayerSource;
|
use codex_config::ConfigLayerSource;
|
||||||
use codex_config::ConfigLayerStack;
|
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::WindowsSandboxLevelExt;
|
||||||
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
use crate::windows_sandbox::resolve_windows_sandbox_mode;
|
||||||
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
use crate::windows_sandbox::resolve_windows_sandbox_private_desktop;
|
||||||
|
use codex_agent_roles::load_agent_roles;
|
||||||
use codex_config::CloudConfigBundleLoader;
|
use codex_config::CloudConfigBundleLoader;
|
||||||
use codex_config::ConfigLayerSource;
|
use codex_config::ConfigLayerSource;
|
||||||
use codex_config::ConfigLayerStack;
|
use codex_config::ConfigLayerStack;
|
||||||
@@ -155,7 +156,6 @@ use codex_network_proxy::NetworkProxyConfig;
|
|||||||
use toml::Value as TomlValue;
|
use toml::Value as TomlValue;
|
||||||
use toml_edit::DocumentMut;
|
use toml_edit::DocumentMut;
|
||||||
|
|
||||||
pub(crate) mod agent_roles;
|
|
||||||
mod auth_keyring;
|
mod auth_keyring;
|
||||||
pub mod edit;
|
pub mod edit;
|
||||||
mod managed_features;
|
mod managed_features;
|
||||||
@@ -170,6 +170,7 @@ mod resolved_permission_profile;
|
|||||||
mod schema;
|
mod schema;
|
||||||
pub use auth_keyring::bootstrap_auth_config;
|
pub use auth_keyring::bootstrap_auth_config;
|
||||||
pub use auth_keyring::resolve_bootstrap_auth_keyring_backend_kind;
|
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::ConfigLoadOptions;
|
||||||
pub use codex_config::Constrained;
|
pub use codex_config::Constrained;
|
||||||
pub use codex_config::ConstraintError;
|
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}")))
|
.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(
|
fn resolve_tool_suggest_config(
|
||||||
config_toml: &ConfigToml,
|
config_toml: &ConfigToml,
|
||||||
config_layer_stack: &ConfigLayerStack,
|
config_layer_stack: &ConfigLayerStack,
|
||||||
@@ -3676,8 +3666,7 @@ impl Config {
|
|||||||
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
|
let terminal_resize_reflow = resolve_terminal_resize_reflow_config(&cfg);
|
||||||
|
|
||||||
let agent_roles =
|
let agent_roles =
|
||||||
agent_roles::load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings)
|
load_agent_roles(fs, &cfg, &config_layer_stack, &mut startup_warnings).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let openai_base_url = cfg
|
let openai_base_url = cfg
|
||||||
.openai_base_url
|
.openai_base_url
|
||||||
|
|||||||
Reference in new issue
Block a user