mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
feat(home): add CODEX_HOME instructions contributor
This commit is contained in:
Generated
+12
@@ -2701,6 +2701,7 @@ dependencies = [
|
||||
"codex-exec-server",
|
||||
"codex-extension-api",
|
||||
"codex-features",
|
||||
"codex-home",
|
||||
"codex-login",
|
||||
"codex-model-provider-info",
|
||||
"codex-models-manager",
|
||||
@@ -3070,6 +3071,17 @@ dependencies = [
|
||||
"codex-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-home"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-extension-api",
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-hooks"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -37,6 +37,7 @@ members = [
|
||||
"core-api",
|
||||
"core-plugins",
|
||||
"core-skills",
|
||||
"home",
|
||||
"hooks",
|
||||
"secrets",
|
||||
"exec",
|
||||
@@ -169,6 +170,7 @@ codex-execpolicy = { path = "execpolicy" }
|
||||
codex-extension-api = { path = "ext/extension-api" }
|
||||
codex-goal-extension = { path = "ext/goal" }
|
||||
codex-guardian = { path = "ext/guardian" }
|
||||
codex-home = { path = "home" }
|
||||
codex-image-generation-extension = { path = "ext/image-generation" }
|
||||
codex-external-agent-migration = { path = "external-agent-migration" }
|
||||
codex-external-agent-sessions = { path = "external-agent-sessions" }
|
||||
|
||||
@@ -22,6 +22,7 @@ codex-core = { workspace = true }
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-models-manager = { workspace = true }
|
||||
|
||||
@@ -55,6 +55,7 @@ pub use codex_extension_api::GlobalInstructionsFuture;
|
||||
pub use codex_extension_api::empty_extension_registry;
|
||||
pub use codex_features::Feature;
|
||||
pub use codex_features::Features;
|
||||
pub use codex_home::CodexHomeInstructionsContributor;
|
||||
pub use codex_login::AuthManager;
|
||||
pub use codex_login::default_client::set_default_originator;
|
||||
pub use codex_model_provider_info::OPENAI_PROVIDER_ID;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "home",
|
||||
crate_name = "codex_home",
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
name = "codex-home"
|
||||
version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_home"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-extension-api = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::io;
|
||||
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_extension_api::GlobalInstructionsContributor;
|
||||
use codex_extension_api::GlobalInstructionsFuture;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
pub const DEFAULT_AGENTS_MD_FILENAME: &str = "AGENTS.md";
|
||||
pub const LOCAL_AGENTS_MD_FILENAME: &str = "AGENTS.override.md";
|
||||
|
||||
/// Loads global instructions from a configured Codex home directory.
|
||||
pub struct CodexHomeInstructionsContributor {
|
||||
codex_home: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl CodexHomeInstructionsContributor {
|
||||
pub fn new(codex_home: AbsolutePathBuf) -> Self {
|
||||
Self { codex_home }
|
||||
}
|
||||
|
||||
async fn load(&self) -> Result<GlobalInstructions, String> {
|
||||
let mut warnings = Vec::new();
|
||||
let mut read_errors = Vec::new();
|
||||
for candidate in [LOCAL_AGENTS_MD_FILENAME, DEFAULT_AGENTS_MD_FILENAME] {
|
||||
let path = self.codex_home.join(candidate);
|
||||
let data = match tokio::fs::read(path.as_path()).await {
|
||||
Ok(data) => data,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(err) if err.kind() == io::ErrorKind::IsADirectory => continue,
|
||||
Err(err) => {
|
||||
let warning = format!(
|
||||
"Failed to read global AGENTS.md instructions from `{}`: {err}",
|
||||
path.display()
|
||||
);
|
||||
warnings.push(warning.clone());
|
||||
read_errors.push(warning);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(err) = std::str::from_utf8(&data) {
|
||||
warnings.push(format!(
|
||||
"Global AGENTS.md instructions from `{}` contain invalid UTF-8: {err}. Invalid byte sequences were replaced.",
|
||||
path.display(),
|
||||
));
|
||||
}
|
||||
let contents = String::from_utf8_lossy(&data);
|
||||
let trimmed = contents.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(trimmed.to_string(), Some(path))],
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if !read_errors.is_empty() {
|
||||
return Err(read_errors.join("; "));
|
||||
}
|
||||
|
||||
Ok(GlobalInstructions {
|
||||
instructions: Vec::new(),
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalInstructionsContributor for CodexHomeInstructionsContributor {
|
||||
fn contribute(&self) -> GlobalInstructionsFuture<'_> {
|
||||
Box::pin(self.load())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lib_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::fs;
|
||||
|
||||
use codex_extension_api::GlobalInstruction;
|
||||
use codex_extension_api::GlobalInstructions;
|
||||
use codex_extension_api::GlobalInstructionsContributor;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::CodexHomeInstructionsContributor;
|
||||
|
||||
fn contributor(temp: &TempDir) -> CodexHomeInstructionsContributor {
|
||||
CodexHomeInstructionsContributor::new(
|
||||
AbsolutePathBuf::try_from(temp.path().to_path_buf()).expect("absolute temp path"),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_override_before_default() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
fs::write(temp.path().join("AGENTS.md"), "default").expect("write default");
|
||||
fs::write(temp.path().join("AGENTS.override.md"), "override").expect("write override");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(
|
||||
"override",
|
||||
Some(
|
||||
AbsolutePathBuf::try_from(temp.path().join("AGENTS.override.md"))
|
||||
.expect("absolute override path")
|
||||
),
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn falls_back_from_empty_override_to_default() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
fs::write(temp.path().join("AGENTS.override.md"), " \n").expect("write override");
|
||||
fs::write(temp.path().join("AGENTS.md"), "default").expect("write default");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(
|
||||
"default",
|
||||
Some(
|
||||
AbsolutePathBuf::try_from(temp.path().join("AGENTS.md"))
|
||||
.expect("absolute default path")
|
||||
),
|
||||
)],
|
||||
warnings: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn falls_back_from_directory_override_to_default() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
fs::create_dir(temp.path().join("AGENTS.override.md")).expect("create override directory");
|
||||
fs::write(temp.path().join("AGENTS.md"), "default").expect("write default");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(
|
||||
actual.instructions,
|
||||
vec![GlobalInstruction::new(
|
||||
"default",
|
||||
Some(
|
||||
AbsolutePathBuf::try_from(temp.path().join("AGENTS.md"))
|
||||
.expect("absolute default path")
|
||||
),
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn whitespace_only_files_return_empty_instructions() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
fs::write(temp.path().join("AGENTS.override.md"), " \n").expect("write override");
|
||||
fs::write(temp.path().join("AGENTS.md"), "\t").expect("write default");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(actual, GlobalInstructions::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_default_without_an_execution_environment() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
fs::write(temp.path().join("AGENTS.md"), "default").expect("write default");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(
|
||||
actual.instructions,
|
||||
vec![GlobalInstruction::new(
|
||||
"default",
|
||||
Some(
|
||||
AbsolutePathBuf::try_from(temp.path().join("AGENTS.md"))
|
||||
.expect("absolute default path")
|
||||
),
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_files_return_empty_instructions() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(actual, Default::default());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_utf8_is_lossy_and_warns() {
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let path = temp.path().join("AGENTS.md");
|
||||
let data = vec![
|
||||
b'g', b'l', b'o', b'b', b'a', b'l', 0xff, b' ', b'd', b'o', b'c',
|
||||
];
|
||||
fs::write(&path, &data).expect("write default");
|
||||
let utf8_error = std::str::from_utf8(&data).expect_err("fixture should be invalid UTF-8");
|
||||
|
||||
let actual = contributor(&temp).contribute().await.expect("load");
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
GlobalInstructions {
|
||||
instructions: vec![GlobalInstruction::new(
|
||||
"global\u{fffd} doc",
|
||||
Some(AbsolutePathBuf::try_from(path.clone()).expect("absolute default path")),
|
||||
)],
|
||||
warnings: vec![format!(
|
||||
"Global AGENTS.md instructions from `{}` contain invalid UTF-8: {utf8_error}. Invalid byte sequences were replaced.",
|
||||
path.display()
|
||||
)],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn unreadable_candidates_fail_instead_of_deleting_the_previous_snapshot() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = TempDir::new().expect("temp dir");
|
||||
let path = temp.path().join("AGENTS.override.md");
|
||||
symlink(&path, &path).expect("create symlink loop");
|
||||
|
||||
let error = contributor(&temp)
|
||||
.contribute()
|
||||
.await
|
||||
.expect_err("unreadable candidate should fail");
|
||||
|
||||
assert!(error.contains("Failed to read global AGENTS.md instructions"));
|
||||
assert!(error.contains(&path.display().to_string()));
|
||||
}
|
||||
Reference in New Issue
Block a user