[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
+62
View File
@@ -23,6 +23,8 @@ use codex_core_plugins::PluginsManager;
use codex_exec_server::EnvironmentManager;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::UserInstructionsProvider;
use codex_extension_api::empty_extension_registry;
use codex_features::Feature;
use codex_login::AuthManager;
@@ -209,6 +211,7 @@ pub(crate) struct ThreadManagerState {
plugins_manager: Arc<PluginsManager>,
mcp_manager: Arc<McpManager>,
extensions: Arc<ExtensionRegistry<Config>>,
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
thread_store: Arc<dyn ThreadStore>,
attestation_provider: Option<Arc<dyn AttestationProvider>>,
session_source: SessionSource,
@@ -259,6 +262,7 @@ impl ThreadManager {
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
extensions: Arc<ExtensionRegistry<Config>>,
user_instructions_provider: Arc<dyn UserInstructionsProvider>,
analytics_events_client: Option<AnalyticsEventsClient>,
thread_store: Arc<dyn ThreadStore>,
state_db: Option<StateDbHandle>,
@@ -292,6 +296,7 @@ impl ThreadManager {
plugins_manager,
mcp_manager,
extensions,
user_instructions_provider,
thread_store,
attestation_provider,
auth_manager,
@@ -394,6 +399,9 @@ impl ThreadManager {
plugins_manager,
mcp_manager,
extensions: empty_extension_registry(),
user_instructions_provider: Arc::new(
crate::test_support::EmptyUserInstructionsProvider,
),
thread_store,
attestation_provider: None,
auth_manager,
@@ -1091,6 +1099,56 @@ impl ThreadManagerState {
resolve_multi_agent_version(initial_history, inherited_multi_agent_version)
}
/// Resolves the provider snapshot for a newly spawned runtime.
///
/// Loads a fresh provider snapshot for:
/// - fresh root threads;
/// - cold resumes;
/// - root forks.
///
/// Uses an existing snapshot for:
/// - subagents, which inherit from their parent without invoking the
/// provider;
/// - running resumes and compaction paths, which retain the live session.
///
/// Provider warnings only apply to fresh loads. If a parent runtime is no
/// longer available, its child starts without provider instructions rather
/// than loading independently.
async fn user_instructions_for_spawn(
&self,
session_source: &SessionSource,
parent_thread_id: Option<ThreadId>,
forked_from_thread_id: Option<ThreadId>,
) -> LoadedUserInstructions {
let is_root_agent = !session_source.is_non_root_agent();
if is_root_agent {
return self
.user_instructions_provider
.load_user_instructions()
.await;
}
let inherited_thread_id = match session_source {
SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id, ..
}) => Some(*parent_thread_id),
_ => parent_thread_id.or(forked_from_thread_id),
};
let instructions = match inherited_thread_id {
// The spawn path retains only thread IDs, so look up the live
// runtime again here to inherit its user instructions.
Some(thread_id) => match self.get_thread(thread_id).await {
Ok(thread) => thread.codex.session.user_instructions().await,
Err(_) => None,
},
None => None,
};
LoadedUserInstructions {
instructions,
warnings: Vec::new(),
}
}
/// Spawn a new thread with no history using a provided config.
pub(crate) async fn spawn_new_thread(
&self,
@@ -1308,6 +1366,9 @@ impl ThreadManagerState {
}
let environment_selections =
resolve_environment_selections(self.environment_manager.as_ref(), &environments)?;
let user_instructions = self
.user_instructions_for_spawn(&session_source, parent_thread_id, forked_from_thread_id)
.await;
let parent_rollout_thread_trace = self
.parent_rollout_thread_trace_for_source(&session_source, &initial_history)
.await;
@@ -1324,6 +1385,7 @@ impl ThreadManagerState {
codex, thread_id, ..
} = Box::pin(Codex::spawn(CodexSpawnArgs {
config,
user_instructions,
installation_id: self.installation_id.clone(),
auth_manager,
models_manager: Arc::clone(&self.models_manager),