mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
committed by
copyberry
parent
85a1b0e33d
commit
ce950dcf26
+6
@@ -8908,6 +8908,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"additionalDeveloperInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
+6
@@ -4966,6 +4966,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"additionalDeveloperInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
+6
@@ -115,6 +115,12 @@
|
||||
},
|
||||
"ConfigRequirements": {
|
||||
"properties": {
|
||||
"additionalDeveloperInstructions": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"allowAppshots": {
|
||||
"type": [
|
||||
"boolean",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
)),
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!();
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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)]),
|
||||
|
||||
Reference in New Issue
Block a user