[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 12:28:47 -07:00
committed by GitHub
Unverified
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()
+3 -3
View File
@@ -55,9 +55,9 @@ pub(crate) struct SessionConfiguration {
/// Developer instructions that supplement the base instructions.
pub(super) developer_instructions: Option<String>,
/// Model instructions that are appended to the base instructions and the
/// files that supplied them.
pub(super) user_instructions: Option<LoadedAgentsMd>,
/// Model instructions assembled from provider instructions and discovered
/// AGENTS.md files.
pub(super) loaded_agents_md: Option<LoadedAgentsMd>,
/// Personality preference for the model.
pub(super) personality: Option<Personality>,
+8 -8
View File
@@ -3286,7 +3286,7 @@ async fn set_rate_limits_retains_previous_credits() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -3393,7 +3393,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -3925,7 +3925,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -4777,7 +4777,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -4885,7 +4885,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5117,7 +5117,7 @@ async fn make_session_with_config_and_rx(
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -5219,7 +5219,7 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -6963,7 +6963,7 @@ where
collaboration_mode,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
loaded_agents_md: None,
service_tier: None,
personality: config.personality,
base_instructions: config
@@ -705,6 +705,7 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() {
let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs {
config,
user_instructions: Default::default(),
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
auth_manager,
models_manager,
+1 -1
View File
@@ -552,7 +552,7 @@ impl Session {
developer_instructions: session_configuration.developer_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration
.user_instructions
.loaded_agents_md
.as_ref()
.map(LoadedAgentsMd::text),
collaboration_mode: session_configuration.collaboration_mode.clone(),