[codex] Load user instructions through an injected provider (#27101)

## Why

We want to remove implicit use of `$CODEX_HOME` from `codex-core` and
make embedders responsible for supplying user-level instructions. This
also ensures user instructions load when no primary environment is
selected.

## What changed

Stacked on #27415, which makes `codex exec` surface thread-scoped
runtime warnings.

- Added `UserInstructionsProvider` to `codex-extension-api`, with
absolute source attribution and recoverable loading warnings.
- Added `codex-home` with the filesystem-backed provider for
`AGENTS.override.md` and `AGENTS.md`, preserving precedence, fallback,
trimming, lossy UTF-8 handling, and the existing uncapped global
instruction size.
- Removed global instruction loading from `Config` and require
`ThreadManager` callers to inject a provider.
- Load provider instructions once for each fresh root runtime, including
runtimes without a primary environment. Running sessions retain their
snapshot, while child agents inherit the parent snapshot without
invoking the provider.
- Keep provider instructions separate while loading project `AGENTS.md`,
then assemble the model-visible instructions with the existing ordering,
source attribution, warning, and turn-context behavior.
- Wired the Codex home provider through the CLI, app server, MCP server,
core facade, and thread-manager sample.

## Validation

- `just test -p codex-home -p codex-extension-api`
- `just test -p codex-core agents_md`
- `just test -p codex-core guardian`
- `just test -p codex-app-server
thread_start_without_selected_environment_includes_only_global_instruction_source`
- `just test -p codex-exec warning`
- `just bazel-lock-check`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-11 19:28:47 +00:00
committed by GitHub
parent b2a4e3be27
commit 236b50125d
49 changed files with 1368 additions and 567 deletions
+31 -12
View File
@@ -14,6 +14,7 @@ use crate::agent::AgentControl;
use crate::agent::AgentStatus;
use crate::agent::agent_status_from_event;
use crate::agent::status::is_final;
use crate::agents_md::LoadedAgentsMd;
use crate::attestation::AttestationProvider;
use crate::build_available_skills;
use crate::compact;
@@ -54,6 +55,7 @@ use codex_exec_server::Environment;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::FileSystemSandboxContext;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::PromptSlot;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -291,7 +293,7 @@ use crate::SkillLoadOutcome;
#[cfg(test)]
use crate::SkillMetadata;
use crate::SkillsManager;
use crate::agents_md::AgentsMdManager;
use crate::agents_md::load_project_instructions;
use crate::context::UserInstructions;
use crate::exec_policy::ExecPolicyUpdateError;
use crate::guardian::GuardianReviewSessionManager;
@@ -399,6 +401,7 @@ pub struct CodexSpawnOk {
pub(crate) struct CodexSpawnArgs {
pub(crate) config: Config,
pub(crate) user_instructions: LoadedUserInstructions,
pub(crate) installation_id: String,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) models_manager: SharedModelsManager,
@@ -484,6 +487,7 @@ impl Codex {
async fn spawn_internal(args: CodexSpawnArgs) -> CodexResult<CodexSpawnOk> {
let CodexSpawnArgs {
mut config,
user_instructions,
installation_id,
auth_manager,
models_manager,
@@ -515,16 +519,21 @@ impl Codex {
let (tx_sub, rx_sub) = async_channel::bounded(SUBMISSION_CHANNEL_CAPACITY);
let (tx_event, rx_event) = async_channel::unbounded();
let LoadedUserInstructions {
instructions: user_instructions,
warnings: user_instruction_provider_warnings,
} = user_instructions;
// TODO(anp) pull startup_warnings out of Config
config
.startup_warnings
.extend(user_instruction_provider_warnings);
// TODO(anp) assemble instructions from multiple environments
let primary_environment = environment_selections.primary_environment();
let mut user_instruction_warnings = Vec::new();
let user_instructions = if let Some(primary_environment) = primary_environment {
AgentsMdManager::new(&config)
.user_instructions(primary_environment.as_ref(), &mut user_instruction_warnings)
.await
} else {
None
};
config.startup_warnings.extend(user_instruction_warnings);
let primary_fs = primary_environment
.as_ref()
.map(|environment| environment.get_filesystem());
let loaded_agents_md =
load_project_instructions(&mut config, user_instructions, primary_fs.as_deref()).await;
let exec_policy = if crate::guardian::is_guardian_reviewer_source(&session_source) {
// Guardian review should rely on the built-in shell safety checks,
@@ -604,7 +613,7 @@ impl Codex {
model_reasoning_summary: config.model_reasoning_summary,
service_tier,
developer_instructions: config.developer_instructions.clone(),
user_instructions,
loaded_agents_md,
personality: config.personality,
base_instructions,
compact_prompt: config.compact_prompt.clone(),
@@ -818,7 +827,7 @@ impl Codex {
let state = self.session.state.lock().await;
state
.session_configuration
.user_instructions
.loaded_agents_md
.as_ref()
.map_or_else(Vec::new, |instructions| {
instructions.sources().cloned().collect()
@@ -1507,6 +1516,16 @@ impl Session {
.clone()
}
pub(crate) async fn user_instructions(&self) -> Option<codex_extension_api::UserInstructions> {
let state = self.state.lock().await;
state
.session_configuration
.loaded_agents_md
.as_ref()
.and_then(LoadedAgentsMd::user_instructions)
.cloned()
}
pub(crate) async fn provider(&self) -> ModelProviderInfo {
let state = self.state.lock().await;
state.session_configuration.provider.clone()