Add managed developer instructions to requirements (#39755)

## What changed

- Add `additional_developer_instructions` to managed requirements and expose it through `configRequirements/read` as `additionalDeveloperInstructions`, independently of ordinary developer instructions.
- Include the managed instructions in model context, emit explicit replacement or removal messages when requirements change, and preserve the current value across compaction, resume, and agent forks without duplication.
- Reject managed instructions whose rendered context exceeds 10,000 estimated tokens.

## Testing

- Cover requirements layering and API serialization, context updates and removal, size validation, repeated model requests, compaction and resume, rollout migration, and agent forks.

GitOrigin-RevId: bc0b70fb7988944c2f68176dff55f5ed61eb46c8
This commit is contained in:
joeflorencio-openai
2026-08-20 17:36:22 +00:00
committed by copyberry
parent 85a1b0e33d
commit ce950dcf26
31 changed files with 540 additions and 19 deletions
@@ -8908,6 +8908,12 @@
},
"ConfigRequirements": {
"properties": {
"additionalDeveloperInstructions": {
"type": [
"string",
"null"
]
},
"allowAppshots": {
"type": [
"boolean",
@@ -4966,6 +4966,12 @@
},
"ConfigRequirements": {
"properties": {
"additionalDeveloperInstructions": {
"type": [
"string",
"null"
]
},
"allowAppshots": {
"type": [
"boolean",
@@ -115,6 +115,12 @@
},
"ConfigRequirements": {
"properties": {
"additionalDeveloperInstructions": {
"type": [
"string",
"null"
]
},
"allowAppshots": {
"type": [
"boolean",
@@ -15,4 +15,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
export type ConfigRequirements = {cliAuthCredentialsStore: CliAuthCredentialsStoreMode | null, chatgptBaseUrl: string | null, additionalDeveloperInstructions: string | null, allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, allowRemoteControl: boolean | null, computerUse: ComputerUseRequirements | null, browserUse: BrowserUseRequirements | null, inAppBrowser: InAppBrowserRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null, autoReview: AutoReviewRequirements | null, models: ModelsRequirements | null, sqliteHome: PathUri | null, logDir: PathUri | null, modelCatalogJson: PathUri | null, checkForUpdateOnStartup: boolean | null, allowLoginShell: boolean | null, feedback: FeedbackRequirements | null, windowsSandboxPrivateDesktop: boolean | null};
@@ -385,6 +385,7 @@ pub struct ConfigReadResponse {
pub struct ConfigRequirements {
pub cli_auth_credentials_store: Option<CliAuthCredentialsStoreMode>,
pub chatgpt_base_url: Option<String>,
pub additional_developer_instructions: Option<String>,
#[experimental(nested)]
pub allowed_approval_policies: Option<Vec<AskForApproval>>,
#[experimental("configRequirements/read.allowedApprovalsReviewers")]
@@ -2042,6 +2042,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
crate::experimental_api::ExperimentalApi::experimental_reason(&ConfigRequirements {
cli_auth_credentials_store: None,
chatgpt_base_url: None,
additional_developer_instructions: None,
allowed_approval_policies: Some(vec![AskForApproval::Granular {
sandbox_approval: true,
rules: true,
+1 -1
View File
@@ -288,7 +288,7 @@ Example with notification opt-out:
- `externalAgentConfig/import/readHistories` — read completed import histories and connector candidates detected from successfully imported session histories. Successful session entries include the original imported title when one was available. Connector candidates include a normalized display `name`, the number of imported sessions that used the connector, and the source metadata field used for detection.
- `config/value/write` — write a single config key/value to the user's config.toml on disk; dotted paths such as `desktop.someKey` use the same generic write surface. Writes that overlap a managed requirement are rejected with `configRequirementReadonly`.
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads, including multiple `desktop.*` edits. Session-static model, reasoning-effort, Plan-mode reasoning-effort, service-tier, and personality defaults do not reload existing threads.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), interactive browser import policy (`inAppBrowser.allowExternalBrowserSettingsImport`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including exact managed values (`cliAuthCredentialsStore`, `chatgptBaseUrl`, `sqliteHome`, `logDir`, `modelCatalogJson`, `checkForUpdateOnStartup`, `allowLoginShell`, `feedback.enabled`, and `windowsSandboxPrivateDesktop`), requirements-only developer instructions (`additionalDeveloperInstructions`, supplied independently of ordinary developer instructions), allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), the layered permission-profile allow map (`allowedPermissionProfiles`), the managed permission-profile default (`defaultPermissions`), lifecycle hook lockdown (`allowManagedHooksOnly`), remote-control policy (`allowRemoteControl`; `false` force-disables remote control while `true` or `null` preserves existing behavior), computer use policy (`computerUse`), Browser Use policy (`browserUse.disableAutoReview`), interactive browser import policy (`inAppBrowser.allowExternalBrowserSettingsImport`), pinned feature values (`featureRequirements`, including the default-allowed `in_app_updates` policy that administrators can set to `false`), managed lifecycle hooks (`hooks`, including command handlers with optional `additionalContextLimit` and `mcp_tool` handlers with `server`, `tool`, `input`, `timeoutSec`, and `statusMessage`), `enforceResidency`, managed automatic review (`autoReview.requiredOnModels` and `autoReview.ignoreRules`), model defaults (`models.newThread.model`, `models.newThread.modelReasoningEffort`, and `models.newThread.serviceTier`), and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
### Example: Start or resume a thread
@@ -371,6 +371,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
},
),
chatgpt_base_url: requirements.chatgpt_base_url,
additional_developer_instructions: requirements.additional_developer_instructions,
allowed_approval_policies: requirements.allowed_approval_policies.map(|policies| {
policies
.into_iter()
@@ -278,12 +278,17 @@ in_app_updates = false
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn config_requirements_read_includes_model_auto_review_and_new_thread_defaults() -> Result<()>
{
async fn config_requirements_read_includes_managed_model_policy_and_instructions() -> Result<()> {
let codex_home = TempDir::new()?;
write_config(
&codex_home,
"developer_instructions = \"ordinary instructions\"\n",
)?;
std::fs::write(
codex_home.path().join("requirements.toml"),
r#"
additional_developer_instructions = "Follow the managed policy.\nPreserve its formatting."
[auto_review]
required_on_models = ["gpt-protected", "gpt-sensitive"]
ignore_rules = ["gpt-protected"]
@@ -305,6 +310,10 @@ service_tier = "fast"
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??;
let requirements = response.requirements.expect("managed requirements");
assert_eq!(
requirements.additional_developer_instructions.as_deref(),
Some("Follow the managed policy.\nPreserve its formatting.")
);
let auto_review = requirements
.auto_review
.expect("managed automatic-review requirements");
@@ -327,6 +336,25 @@ service_tier = "fast"
Some(ReasoningEffort::Medium)
);
assert_eq!(defaults.service_tier.as_deref(), Some("fast"));
let config_id = mcp
.send_config_read_request(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
let config: ConfigReadResponse =
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(config_id)).await??;
assert_eq!(
(
config.config.developer_instructions.as_deref(),
config
.config
.additional
.get("additional_developer_instructions"),
),
(Some("ordinary instructions"), None),
);
Ok(())
}
@@ -187,6 +187,8 @@ pub struct ConfigRequirements {
pub network: Option<Sourced<NetworkConstraints>>,
/// Managed filesystem constraints derived from requirements.
pub filesystem: Option<Sourced<FilesystemConstraints>>,
/// Managed instructions included independently of ordinary developer instructions.
pub additional_developer_instructions: Option<Sourced<String>>,
/// Source for the managed guardian policy config, when one is configured.
pub guardian_policy_config_source: Option<RequirementSource>,
}
@@ -242,6 +244,7 @@ impl Default for ConfigRequirements {
),
network: None,
filesystem: None,
additional_developer_instructions: None,
guardian_policy_config_source: None,
}
}
@@ -959,6 +962,7 @@ pub struct ConfigRequirementsToml {
pub permissions: Option<PermissionsRequirementsToml>,
pub auto_review: Option<AutoReviewRequirementsToml>,
pub models: Option<ModelsRequirementsToml>,
pub additional_developer_instructions: Option<String>,
pub guardian_policy_config: Option<String>,
}
@@ -1059,6 +1063,7 @@ pub struct ConfigRequirementsWithSources {
pub permissions: Option<Sourced<PermissionsRequirementsToml>>,
pub auto_review: Option<Sourced<AutoReviewRequirementsToml>>,
pub models: Option<Sourced<ModelsRequirementsToml>>,
pub additional_developer_instructions: Option<Sourced<String>>,
pub guardian_policy_config: Option<Sourced<String>>,
}
@@ -1117,6 +1122,7 @@ impl ConfigRequirementsWithSources {
permissions: _,
auto_review: _,
models: _,
additional_developer_instructions: _,
guardian_policy_config: _,
} = &other;
@@ -1166,6 +1172,7 @@ impl ConfigRequirementsWithSources {
network,
permissions,
models,
additional_developer_instructions,
guardian_policy_config,
}
);
@@ -1248,6 +1255,7 @@ impl ConfigRequirementsWithSources {
permissions,
auto_review,
models,
additional_developer_instructions,
guardian_policy_config,
} = self;
ConfigRequirementsToml {
@@ -1287,6 +1295,8 @@ impl ConfigRequirementsWithSources {
permissions: permissions.map(|sourced| sourced.value),
auto_review: auto_review.map(|sourced| sourced.value),
models: models.map(|sourced| sourced.value),
additional_developer_instructions: additional_developer_instructions
.map(|sourced| sourced.value),
guardian_policy_config: guardian_policy_config.map(|sourced| sourced.value),
}
}
@@ -1431,6 +1441,7 @@ impl ConfigRequirementsToml {
.models
.as_ref()
.is_none_or(ModelsRequirementsToml::is_empty)
&& self.additional_developer_instructions.is_none()
&& self
.guardian_policy_config
.as_deref()
@@ -1606,6 +1617,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
permissions,
auto_review,
models: _,
additional_developer_instructions,
guardian_policy_config,
} = toml;
@@ -1962,6 +1974,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
enforce_residency,
network,
filesystem,
additional_developer_instructions,
guardian_policy_config_source,
})
}
@@ -2134,6 +2147,7 @@ mod tests {
permissions,
auto_review,
models,
additional_developer_instructions,
guardian_policy_config,
} = toml;
ConfigRequirementsWithSources {
@@ -2191,6 +2205,8 @@ mod tests {
permissions: permissions.map(|value| Sourced::new(value, RequirementSource::Unknown)),
auto_review: auto_review.map(|value| Sourced::new(value, RequirementSource::Unknown)),
models: models.map(|value| Sourced::new(value, RequirementSource::Unknown)),
additional_developer_instructions: additional_developer_instructions
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
guardian_policy_config: guardian_policy_config
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
}
@@ -2445,6 +2461,7 @@ mod tests {
};
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();
let additional_developer_instructions = "Follow the company policy.".to_string();
let guardian_policy_config = "Use the company-managed guardian policy.".to_string();
// Intentionally constructed without `..Default::default()` so adding a new field to
@@ -2486,6 +2503,7 @@ mod tests {
permissions: None,
auto_review: Some(auto_review.clone()),
models: Some(models.clone()),
additional_developer_instructions: Some(additional_developer_instructions.clone()),
guardian_policy_config: Some(guardian_policy_config.clone()),
};
@@ -2565,6 +2583,10 @@ mod tests {
permissions: None,
auto_review: Some(Sourced::new(auto_review, source.clone())),
models: Some(Sourced::new(models, source.clone())),
additional_developer_instructions: Some(Sourced::new(
additional_developer_instructions,
source.clone(),
)),
guardian_policy_config: Some(Sourced::new(guardian_policy_config, source)),
}
);
@@ -247,6 +247,7 @@ fn populate_merged_regular_fields_with_sources(
permissions,
auto_review,
models,
additional_developer_instructions,
guardian_policy_config,
} = requirements;
@@ -292,6 +293,10 @@ fn populate_merged_regular_fields_with_sources(
set_sourced!(network, &["experimental_network"]);
set_sourced!(permissions, &["permissions"]);
set_sourced!(models, &["models"]);
set_sourced!(
additional_developer_instructions,
&["additional_developer_instructions"]
);
if let Some(guardian_policy_config) =
guardian_policy_config.filter(|value| !value.trim().is_empty())
@@ -96,6 +96,7 @@ allowed_approval_policies = ["on-request"]
allowed_sandbox_modes = ["workspace-write"]
default_permissions = ":workspace"
allow_remote_control = true
additional_developer_instructions = "Lower-priority instructions."
[allowed_permission_profiles]
":read-only" = true
@@ -110,6 +111,7 @@ allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
default_permissions = ":read-only"
allow_remote_control = false
additional_developer_instructions = ""
[allowed_permission_profiles]
":danger-full-access" = false
@@ -128,6 +130,7 @@ allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
default_permissions = ":read-only"
allow_remote_control = false
additional_developer_instructions = ""
[allowed_permission_profiles]
":danger-full-access" = false
+17 -4
View File
@@ -4,6 +4,7 @@ use crate::agent::role::apply_role_to_config;
use crate::config::PermissionProfileSnapshot;
use crate::context::ContextualUserFragment;
use crate::context::CurrentTimeReminder;
use crate::context::ManagedDeveloperInstructions;
use crate::context::MultiAgentModeInstructions;
use crate::context::MultiAgentRoleInstructions;
use crate::session::multi_agents::resolve_usage_hints;
@@ -737,16 +738,28 @@ impl AgentControl {
return false;
}
if let Some(parent_developer_instructions) = parent_developer_instructions.as_ref()
&& let Some(subagent_developer_instructions) =
subagent_developer_instructions.as_ref()
&& let ResponseItem::Message { role, content, .. } = response_item
if let ResponseItem::Message { role, content, .. } = response_item
&& role == "developer"
{
content.retain_mut(|content_item| {
let ContentItem::InputText { text } = content_item else {
return true;
};
if ManagedDeveloperInstructions::matches_text(text) {
// If the child will rebuild its initial context, drop the inherited
// managed instructions; startup will add the current requirements once.
return preserve_reference_context_item;
}
let (
Some(parent_developer_instructions),
Some(subagent_developer_instructions),
) = (
parent_developer_instructions.as_ref(),
subagent_developer_instructions.as_ref(),
)
else {
return true;
};
// TODO(anp) track better message fragment provenance in rollouts.
if !text.contains(parent_developer_instructions) {
return true;
+54 -7
View File
@@ -9,6 +9,7 @@ use crate::config::AgentRoleConfig;
use crate::config::Config;
use crate::config::ConfigBuilder;
use crate::context::ContextualUserFragment;
use crate::context::ManagedDeveloperInstructions;
use crate::context::MultiAgentRoleInstructions;
use crate::context::SubagentNotification;
use crate::init_state_db;
@@ -1306,6 +1307,7 @@ async fn spawn_agent_numeric_fork_from_compacted_paginated_parent_clamps_to_prov
#[tokio::test]
async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
let managed_fragment = "<managed_developer_instructions>\nParent developer instructions.\n</managed_developer_instructions>";
let harness = AgentControlHarness::new().await;
let mut parent_config = harness.config.clone();
let _ = parent_config.features.enable(Feature::MultiAgentV2);
@@ -1388,6 +1390,9 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
ContentItem::InputText {
text: "Preserved developer context.".to_string(),
},
ContentItem::InputText {
text: managed_fragment.to_string(),
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
@@ -1471,6 +1476,9 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
ContentItem::InputText {
text: "Preserved developer context.".to_string(),
},
ContentItem::InputText {
text: managed_fragment.to_string(),
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
@@ -1554,10 +1562,14 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
assert!(
!history_contains_text(
no_hint_history.raw_items(),
"Parent developer instructions."
"Developer context before.\nParent developer instructions."
),
"empty child developer instructions should remove parent developer instructions"
);
assert!(
history_contains_text(no_hint_history.raw_items(), managed_fragment),
"clearing child instructions must preserve an overlapping managed policy"
);
assert!(
history_contains_text(
no_hint_history.raw_items(),
@@ -1934,6 +1946,12 @@ async fn spawn_agent_full_fork_restores_instructions_after_compaction_discards_p
/// rebuild configured developer instructions exactly once.
#[tokio::test]
async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_once() {
let managed_policy = "Managed policy for every agent.";
let current_managed_fragment = format!(
"<managed_developer_instructions>\n{managed_policy}\n</managed_developer_instructions>"
);
let stale_managed_fragment =
"<managed_developer_instructions>\nOld managed policy.\n</managed_developer_instructions>";
for (case, parent_developer_instructions) in [
("without parent instructions", None),
(
@@ -1945,6 +1963,23 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
let mut parent_config = harness.config.clone();
let _ = parent_config.features.enable(Feature::MultiAgentV2);
parent_config.developer_instructions = parent_developer_instructions.map(str::to_string);
let mut requirements = parent_config.config_layer_stack.requirements().clone();
requirements.additional_developer_instructions = Some(codex_config::Sourced::new(
managed_policy.to_string(),
codex_config::RequirementSource::Unknown,
));
let mut requirements_toml = parent_config.config_layer_stack.requirements_toml().clone();
requirements_toml.additional_developer_instructions = Some(managed_policy.to_string());
parent_config.config_layer_stack = codex_config::ConfigLayerStack::new(
parent_config
.config_layer_stack
.all_layers_low_to_high()
.cloned()
.collect(),
requirements,
requirements_toml,
)
.expect("managed requirements stack");
let mut child_config = parent_config.clone();
child_config.developer_instructions = Some("Child developer instructions.".to_string());
child_config.multi_agent_v2.subagent_developer_instructions =
@@ -2004,6 +2039,15 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
internal_chat_message_metadata_passthrough: None,
}));
}
rollout_items.push(rollout_response_item(ResponseItem::Message {
id: None,
role: "developer".to_string(),
content: vec![ContentItem::InputText {
text: stale_managed_fragment.to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: None,
}));
rollout_items.push(RolloutItem::TurnContext(
turn_context.to_turn_context_item(),
));
@@ -2060,6 +2104,7 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
}
let history = child_thread.session.clone_history().await;
let mut instruction_count = 0;
let mut managed_instructions = Vec::new();
for item in history.raw_items() {
let ResponseItem::Message { role, content, .. } = item else {
continue;
@@ -2068,16 +2113,18 @@ async fn spawn_agent_full_fork_legacy_compaction_rebuilds_child_instructions_onc
continue;
}
for content_item in content {
if let ContentItem::InputText { text } = content_item
&& text == "Child developer instructions."
{
instruction_count += 1;
if let ContentItem::InputText { text } = content_item {
instruction_count += usize::from(text == "Child developer instructions.");
if ManagedDeveloperInstructions::matches_text(text) {
managed_instructions.push(text.as_str());
}
}
}
}
assert_eq!(
instruction_count, 1,
"{case}: canonical context reconstruction must not duplicate child developer instructions"
(instruction_count, managed_instructions),
(1, vec![current_managed_fragment.as_str()]),
"{case}: canonical context reconstruction must keep only the current child and managed developer instructions"
);
let _ = harness
+33
View File
@@ -9858,6 +9858,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
permissions: None,
auto_review: None,
models: None,
additional_developer_instructions: None,
guardian_policy_config: None,
};
let requirement_source = codex_config::RequirementSource::Unknown;
@@ -10518,6 +10519,38 @@ async fn test_untrusted_project_gets_unless_trusted_approval_policy() -> anyhow:
Ok(())
}
#[tokio::test]
async fn oversized_managed_developer_instructions_are_rejected_during_config_load()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
// The policy text alone fits; rendering its required context markers must not.
let instructions = "x".repeat(codex_utils_string::approx_bytes_for_tokens(
/*tokens*/ 10_000,
));
let error = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(codex_home.path().to_path_buf()))
.cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
"additional_developer_instructions = {instructions:?}"
)),
)
.build()
.await
.expect_err("oversized managed instructions must fail during config loading");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
let message = error.to_string();
assert!(
message.starts_with(
"`additional_developer_instructions` from enterprise-managed requirements Base requirements (req_1) exceeds the model-context limit of 10000 estimated tokens"
),
"unexpected config-load error: {message}"
);
Ok(())
}
#[tokio::test]
async fn requirements_disallowing_default_sandbox_falls_back_to_required_default()
-> std::io::Result<()> {
+8
View File
@@ -1,5 +1,6 @@
use crate::config::edit::ConfigEdit;
use crate::config::edit::ConfigEditsBuilder;
use crate::context::world_state::validate_managed_developer_instructions;
use crate::guardian::BUNDLED_GUARDIAN_POLICY;
use crate::path_utils::normalize_for_native_workdir;
use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS;
@@ -3108,6 +3109,12 @@ impl Config {
std::io::Error::new(std::io::ErrorKind::InvalidInput, message)
})?;
}
validate_managed_developer_instructions(
config_layer_stack
.requirements()
.additional_developer_instructions
.as_ref(),
)?;
let orchestrator = cfg.orchestrator.as_ref();
let orchestrator_skills_enabled =
resolve_orchestrator_feature_enabled(orchestrator.and_then(|value| value.skills.as_ref()));
@@ -3156,6 +3163,7 @@ impl Config {
enforce_residency,
network: network_requirements,
filesystem: filesystem_requirements,
additional_developer_instructions: _,
guardian_policy_config_source: _,
} = config_layer_stack.requirements().clone();
+1
View File
@@ -93,3 +93,4 @@ pub(crate) use token_budget_context::TokenBudgetReminder;
pub(crate) use turn_aborted::TurnAborted;
pub(crate) use user_instructions::UserInstructions;
pub(crate) use user_shell_command::UserShellCommand;
pub(crate) use world_state::ManagedDeveloperInstructions;
@@ -0,0 +1,152 @@
use super::PreviousSectionState;
use super::WorldStateHash;
use super::WorldStateSection;
use crate::context::ContextualUserFragment;
use codex_config::Sourced;
use codex_utils_string::approx_bytes_for_tokens;
use codex_utils_string::approx_tokens_from_byte_count;
use serde::Deserialize;
use serde::Serialize;
use std::io;
const MAX_MANAGED_DEVELOPER_INSTRUCTIONS_TOKENS: usize = 10_000;
const REPLACEMENT_NOTICE: &str = "These managed developer instructions replace all previously provided managed developer instructions.";
const REMOVAL_NOTICE: &str =
"The previously provided managed developer instructions no longer apply.";
/// A requirements-owned developer message, kept separate from client instructions.
#[derive(Clone, Debug)]
pub(crate) struct ManagedDeveloperInstructions {
instructions: String,
}
impl ContextualUserFragment for ManagedDeveloperInstructions {
fn role(&self) -> &'static str {
"developer"
}
fn requires_separate_message(&self) -> bool {
true
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
}
fn type_markers() -> (&'static str, &'static str) {
(
"<managed_developer_instructions>",
"</managed_developer_instructions>",
)
}
fn body(&self) -> String {
format!("\n{}\n", self.instructions)
}
}
/// Reject oversized policy rather than silently dropping part of its instructions.
pub(crate) fn validate_managed_developer_instructions(
requirement: Option<&Sourced<String>>,
) -> io::Result<()> {
let Some(requirement) = requirement else {
return Ok(());
};
// Reserve the notice used when an existing conversation receives a new policy.
let overhead = ManagedDeveloperInstructions {
instructions: format!("{REPLACEMENT_NOTICE}\n\n"),
}
.render()
.len();
let rendered_bytes = requirement.value.len().saturating_add(overhead);
if rendered_bytes > approx_bytes_for_tokens(MAX_MANAGED_DEVELOPER_INSTRUCTIONS_TOKENS) {
let estimated_tokens = approx_tokens_from_byte_count(rendered_bytes);
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"`additional_developer_instructions` from {} exceeds the model-context limit of {MAX_MANAGED_DEVELOPER_INSTRUCTIONS_TOKENS} estimated tokens ({estimated_tokens} including context markers)",
requirement.source,
),
));
}
Ok(())
}
/// The managed policy currently visible in retained model history.
#[derive(Clone, Debug)]
pub(crate) struct ManagedDeveloperInstructionsState {
instructions: Option<ManagedDeveloperInstructions>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ManagedDeveloperInstructionsSnapshot {
instructions: Option<WorldStateHash>,
}
impl ManagedDeveloperInstructionsState {
pub(crate) fn new(requirement: Option<&Sourced<String>>) -> Self {
Self {
instructions: requirement
.filter(|requirement| !requirement.value.is_empty())
.map(|requirement| ManagedDeveloperInstructions {
instructions: requirement.value.clone(),
}),
}
}
}
impl WorldStateSection for ManagedDeveloperInstructionsState {
const ID: &'static str = "managed_developer_instructions";
type Snapshot = ManagedDeveloperInstructionsSnapshot;
fn snapshot(&self) -> Self::Snapshot {
ManagedDeveloperInstructionsSnapshot {
instructions: self
.instructions
.as_ref()
.map(WorldStateHash::from_fragment),
}
}
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
role == "developer" && ManagedDeveloperInstructions::matches_text(text)
}
fn has_retained_fragment_matcher() -> bool {
true
}
fn matches_retained_fragment(role: &str, text: &str) -> bool {
Self::matches_legacy_fragment(role, text)
}
fn render_diff(
&self,
previous: PreviousSectionState<'_, Self::Snapshot>,
) -> Option<Box<dyn ContextualUserFragment>> {
if matches!(previous, PreviousSectionState::Known(previous) if previous == &self.snapshot())
{
return None;
}
let previous_had_instructions = match previous {
PreviousSectionState::Absent => false,
PreviousSectionState::Unknown => true,
PreviousSectionState::Known(previous) => previous.instructions.is_some(),
};
let fragment = match (&self.instructions, previous_had_instructions) {
(Some(instructions), false) => instructions.clone(),
(Some(instructions), true) => ManagedDeveloperInstructions {
instructions: format!("{REPLACEMENT_NOTICE}\n\n{}", instructions.instructions),
},
(None, true) => ManagedDeveloperInstructions {
instructions: REMOVAL_NOTICE.to_string(),
},
(None, false) => return None,
};
Some(Box::new(fragment))
}
}
#[cfg(test)]
#[path = "managed_developer_instructions_tests.rs"]
mod tests;
@@ -0,0 +1,93 @@
use super::*;
use crate::context::world_state::WorldState;
use codex_config::RequirementSource;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
#[test]
fn managed_policy_updates_and_removal_are_emitted_once() {
let mut history: Vec<ResponseItem> = Vec::new();
let mut previous = None;
for (instructions, expected) in [
(None, None),
(Some("initial policy"), Some("initial policy".to_string())),
(
Some("updated policy"),
Some(format!("{REPLACEMENT_NOTICE}\n\nupdated policy")),
),
(Some(""), Some(REMOVAL_NOTICE.to_string())),
(None, None),
] {
let requirement =
instructions.map(|text| Sourced::new(text.to_string(), RequirementSource::Unknown));
let mut world_state = WorldState::default();
world_state.add_section(ManagedDeveloperInstructionsState::new(requirement.as_ref()));
let updates = world_state
.render_history_diff(previous.as_ref(), &history)
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
assert_eq!(
updates,
expected
.map(|instructions| {
ContextualUserFragment::into(ManagedDeveloperInstructions { instructions })
})
.into_iter()
.collect::<Vec<_>>()
);
history.extend(updates);
previous = Some(world_state.snapshot());
assert!(
world_state
.render_history_diff(previous.as_ref(), &history)
.is_empty()
);
}
let legacy_policy = ContextualUserFragment::into(ManagedDeveloperInstructions {
instructions: "legacy policy".to_string(),
});
for (instructions, expected) in [
(
Some("current policy"),
format!("{REPLACEMENT_NOTICE}\n\ncurrent policy"),
),
(None, REMOVAL_NOTICE.to_string()),
] {
let requirement =
instructions.map(|text| Sourced::new(text.to_string(), RequirementSource::Unknown));
let mut world_state = WorldState::default();
world_state.add_section(ManagedDeveloperInstructionsState::new(requirement.as_ref()));
assert_eq!(
world_state
.render_history_diff(/*previous*/ None, std::slice::from_ref(&legacy_policy))
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>(),
vec![ContextualUserFragment::into(ManagedDeveloperInstructions {
instructions: expected,
})]
);
}
}
#[test]
fn managed_policy_limit_includes_replacement_notice_and_markers() -> io::Result<()> {
let overhead = ManagedDeveloperInstructions {
instructions: format!("{REPLACEMENT_NOTICE}\n\n"),
}
.render()
.len();
let mut requirement = Sourced::new(
"x".repeat(approx_bytes_for_tokens(MAX_MANAGED_DEVELOPER_INSTRUCTIONS_TOKENS) - overhead),
RequirementSource::Unknown,
);
validate_managed_developer_instructions(Some(&requirement))?;
requirement.value.push('x');
let error = validate_managed_developer_instructions(Some(&requirement))
.expect_err("oversized policy must not be silently truncated");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
Ok(())
}
@@ -5,6 +5,7 @@ mod compact_permissions;
mod context_window_guidance;
mod environment;
mod environments_instructions;
mod managed_developer_instructions;
mod model;
mod multi_agent_mode;
mod multi_agent_usage_hint;
@@ -40,6 +41,9 @@ pub(crate) use compact_permissions::CompactPermissionsState;
pub(crate) use context_window_guidance::ContextWindowGuidanceState;
pub(crate) use environment::EnvironmentsState;
pub(crate) use environments_instructions::EnvironmentsInstructionsState;
pub(crate) use managed_developer_instructions::ManagedDeveloperInstructions;
pub(crate) use managed_developer_instructions::ManagedDeveloperInstructionsState;
pub(crate) use managed_developer_instructions::validate_managed_developer_instructions;
pub(crate) use model::ModelInstructionsState;
pub(crate) use multi_agent_mode::MultiAgentModeState;
pub(crate) use multi_agent_usage_hint::MultiAgentUsageHintState;
@@ -1193,6 +1193,9 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
assistant_msg("session prefix item"),
user_input_text_msg("turn 1 user"),
assistant_msg("turn 1 assistant"),
developer_msg(
"<managed_developer_instructions>\nROLLED_BACK_MANAGED_INSTRUCTIONS\n</managed_developer_instructions>",
),
developer_msg(&format!(
"{APPS_INSTRUCTIONS_OPEN_TAG}\nROLLED_BACK_APPS_INSTRUCTIONS"
)),
+1
View File
@@ -41,6 +41,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
"<permissions instructions>",
APPROVED_COMMAND_PREFIX_SAVED_MESSAGE_PREFIX,
"<model_switch>",
"<managed_developer_instructions>",
APPS_INSTRUCTIONS_OPEN_TAG,
COLLABORATION_MODE_OPEN_TAG,
"<multi_agent_role>",
+10
View File
@@ -21,6 +21,7 @@ use crate::compact::CompactedHistoryMetadata;
use crate::config::ManagedFeatures;
use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::context::ContextualUserFragment;
use crate::context::ManagedDeveloperInstructions;
use crate::context::ModelSwitchInstructions;
use crate::context::MultiAgentRoleInstructions;
use crate::context::NetworkRuleSaved;
@@ -3646,6 +3647,7 @@ impl Session {
}
// Render the active mode after the usage hint so it can override that hint.
let mut initial_multi_agent_mode = None;
let mut managed_developer_instructions = None;
for fragment in world_state.render_full() {
match fragment.role() {
"developer"
@@ -3657,6 +3659,11 @@ impl Session {
"developer" if fragment.markers().0 == MULTI_AGENT_MODE_OPEN_TAG => {
initial_multi_agent_mode = Some(fragment);
}
"developer"
if fragment.markers().0 == ManagedDeveloperInstructions::type_markers().0 =>
{
managed_developer_instructions = Some(fragment);
}
"developer"
if fragment.markers().0 == MultiAgentRoleInstructions::type_markers().0 =>
{
@@ -3706,6 +3713,9 @@ impl Session {
{
items.push(guardian_developer_message);
}
if let Some(managed_developer_instructions) = managed_developer_instructions {
items.push(managed_developer_instructions.into_boxed_response_item());
}
// New context windows and compaction install these items directly into replacement history.
for item in &mut items {
item.set_turn_id_if_missing(&turn_context.sub_id);
+9
View File
@@ -12,6 +12,7 @@ use crate::context::world_state::CompactPermissionsState;
use crate::context::world_state::ContextWindowGuidanceState;
use crate::context::world_state::EnvironmentsInstructionsState;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::ManagedDeveloperInstructionsState;
use crate::context::world_state::ModelInstructionsState;
use crate::context::world_state::MultiAgentModeState;
use crate::context::world_state::MultiAgentUsageHintState;
@@ -284,6 +285,14 @@ impl Session {
world_state.add_section(usage_hint);
}
world_state.add_section(multi_agent_mode);
world_state.add_section(ManagedDeveloperInstructionsState::new(
turn_context
.config
.config_layer_stack
.requirements()
.additional_developer_instructions
.as_ref(),
));
Ok(world_state)
}
}
+44
View File
@@ -1,3 +1,4 @@
use codex_config::test_support::CloudConfigBundleFixture;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::ModelClient;
use codex_core::NewThread;
@@ -72,6 +73,7 @@ use core_test_support::responses::mount_sse_once_match;
use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
use core_test_support::responses::sse_failed;
use core_test_support::responses::start_mock_server;
use core_test_support::responses::strip_metadata_from_json;
use core_test_support::responses::strip_response_item_ids_from_json;
use core_test_support::responses_metadata as test_responses_metadata;
@@ -2893,6 +2895,48 @@ async fn includes_developer_instructions_message_in_request() {
);
}
#[tokio::test]
async fn includes_managed_developer_instructions_once_per_request() -> anyhow::Result<()> {
const CLIENT_INSTRUCTIONS: &str = "client developer instructions";
const MANAGED_INSTRUCTIONS: &str = "managed requirements instructions";
let server = start_mock_server().await;
let mut builder = test_codex()
.with_cloud_config_bundle(
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
"additional_developer_instructions = {MANAGED_INSTRUCTIONS:?}"
)),
)
.with_config(|config| {
config.developer_instructions = Some(CLIENT_INSTRUCTIONS.to_string());
});
let test = builder.build_with_auto_env(&server).await?;
let managed_message = format!(
"<managed_developer_instructions>\n{MANAGED_INSTRUCTIONS}\n</managed_developer_instructions>"
);
for (response_id, prompt) in [("resp-1", "first turn"), ("resp-2", "second turn")] {
let response = mount_sse_once(&server, sse(vec![ev_completed(response_id)])).await;
test.submit_text_turn(prompt).await?;
let developer_messages = response.single_request().message_input_texts("developer");
assert!(
developer_messages
.iter()
.any(|message| message.contains(CLIENT_INSTRUCTIONS))
);
assert_eq!(
developer_messages
.iter()
.filter(|message| message.contains("<managed_developer_instructions>"))
.collect::<Vec<_>>(),
vec![&managed_message]
);
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn azure_responses_request_does_not_store_and_preserves_prefixed_item_ids() {
skip_if_no_network!();
+22 -4
View File
@@ -6,6 +6,7 @@ use anyhow::Context;
use anyhow::Result;
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_core::StartThreadOptions;
use codex_core::TurnInputRequest;
use codex_core::X_CODEX_ROUTING_HINT_HEADER;
@@ -3154,9 +3155,14 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
let server = wiremock::MockServer::start().await;
let stale_developer_message = "STALE_DEVELOPER_INSTRUCTIONS_SHOULD_BE_REMOVED";
let managed_requirements = CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"additional_developer_instructions = "MANAGED_DEVELOPER_INSTRUCTIONS""#,
);
let managed_message = "<managed_developer_instructions>\nMANAGED_DEVELOPER_INSTRUCTIONS\n</managed_developer_instructions>";
let mut start_builder =
test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let mut start_builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_cloud_config_bundle(managed_requirements.clone());
let initial = start_builder.build(&server).await?;
let home = initial.home.clone();
let rollout_path = initial
@@ -3233,8 +3239,9 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
})
.await;
let mut resume_builder =
test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let mut resume_builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_cloud_config_bundle(managed_requirements);
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
resumed
@@ -3249,6 +3256,17 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
assert_eq!(compact_mock.requests().len(), 1);
let requests = responses_mock.requests();
assert_eq!(requests.len(), 3, "expected three model requests");
for request in &requests {
assert_eq!(
request
.message_input_texts("developer")
.into_iter()
.filter(|message| message.contains("<managed_developer_instructions>"))
.collect::<Vec<_>>(),
vec![managed_message.to_string()],
"Each model request should contain exactly one managed developer-instructions block, including after compaction and resume."
);
}
let after_compact_request = &requests[1];
let after_resume_request = &requests[2];
@@ -88,6 +88,7 @@ fn is_known_contextual_developer_message_content(content: &[ContentItem]) -> boo
[
"<permissions instructions>",
"<model_switch>",
"<managed_developer_instructions>",
"<apps_instructions>",
"<collaboration_mode>",
"<multi_agent_mode>",
@@ -716,6 +716,10 @@ async fn migration_rolls_back_response_and_inter_agent_user_boundaries() {
SessionSource::Cli,
vec![
rollout_response_item(input_response_message("user", "keep first boundary")),
rollout_response_item(input_response_message(
"developer",
"<managed_developer_instructions>context only</managed_developer_instructions>",
)),
rollout_response_item(input_response_message(
"developer",
"<permissions instructions>context only</permissions instructions>",
+1
View File
@@ -960,6 +960,7 @@ interrupt_message = false
allowed_sandbox_implementations: None,
sandbox_private_desktop: Some(false),
}),
additional_developer_instructions: None,
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
feature_requirements: Some(FeatureRequirementsToml {
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),