Track permission instructions in world state (#33944)

## What changed

- Model permission instructions as a world-state section keyed by a stable hash of their rendered developer message.
- Re-emit permission context when its model-visible contents change or the retained fragment is missing, while avoiding duplicates when matching instructions already exist in history, including bundled developer messages.
- Normalize CRLF line endings when hashing world-state fragments so equivalent content produces the same snapshot.

## Testing

- Add snapshot coverage for unchanged, changed, absent, and unknown permission state.
- Cover bundled retained permissions and restored world-state baselines during rollout reconstruction.

GitOrigin-RevId: 7d8d212460c1690ca617859f66dcd825987f1e23
This commit is contained in:
pakrym-oai
2026-07-18 02:45:55 +00:00
committed by copyberry
parent 6bd3f5e3db
commit 82b294c73c
22 changed files with 396 additions and 189 deletions
@@ -3,6 +3,7 @@ mod apps_instructions;
mod collaboration_mode;
mod environment;
mod environments_instructions;
mod permissions;
mod plugins_instructions;
mod realtime;
#[cfg(test)]
@@ -19,6 +20,8 @@ use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Map;
use serde_json::Value;
use sha1::Digest;
use sha1::Sha1;
use std::collections::BTreeMap;
use std::fmt;
@@ -27,6 +30,7 @@ pub(crate) use apps_instructions::AppsInstructionsState;
pub(crate) use collaboration_mode::CollaborationModeState;
pub(crate) use environment::EnvironmentsState;
pub(crate) use environments_instructions::EnvironmentsInstructionsState;
pub(crate) use permissions::PermissionsState;
pub(crate) use plugins_instructions::PluginsInstructionsState;
pub(crate) use realtime::RealtimeState;
@@ -211,6 +215,27 @@ pub(crate) trait WorldStateSection: Send + Sync + 'static {
) -> Option<Box<dyn ContextualUserFragment>>;
}
/// Stable fingerprint of a model-visible World State fragment.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(transparent)]
pub(crate) struct WorldStateHash(String);
impl WorldStateHash {
pub(crate) fn from_fragment(fragment: &(impl ContextualUserFragment + ?Sized)) -> Self {
let mut hasher = Sha1::new();
hasher.update(b"codex-world-state-fragment-v1\0");
hash_component(&mut hasher, fragment.role());
hash_component(&mut hasher, &fragment.render());
Self(format!("{:x}", hasher.finalize()))
}
}
fn hash_component(hasher: &mut Sha1, value: &str) {
let value = value.replace("\r\n", "\n");
hasher.update((value.len() as u64).to_be_bytes());
hasher.update(value.as_bytes());
}
/// Live model-visible state, keyed by the same stable section IDs used in rollouts.
#[derive(Default)]
pub(crate) struct WorldState {
@@ -0,0 +1,80 @@
use super::PreviousSectionState;
use super::WorldStateHash;
use super::WorldStateSection;
use crate::context::ApprovalPromptContext;
use crate::context::ContextualUserFragment;
use crate::context::PermissionsInstructions;
use codex_execpolicy::Policy;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use std::path::Path;
/// Permission instructions currently visible to the model.
#[derive(Clone, Debug)]
pub(crate) struct PermissionsState {
snapshot: WorldStateHash,
instructions: PermissionsInstructions,
}
impl PermissionsState {
pub(crate) fn new(
permission_profile: &PermissionProfile,
approval_policy: AskForApproval,
approval_context: ApprovalPromptContext<'_>,
exec_policy: &Policy,
cwd: &Path,
exec_permission_approvals_enabled: bool,
request_permissions_tool_enabled: bool,
) -> Self {
let instructions = PermissionsInstructions::from_permission_profile(
permission_profile,
approval_policy,
approval_context,
exec_policy,
cwd,
exec_permission_approvals_enabled,
request_permissions_tool_enabled,
);
let snapshot = WorldStateHash::from_fragment(&instructions);
Self {
snapshot,
instructions,
}
}
}
impl WorldStateSection for PermissionsState {
const ID: &'static str = "permissions";
type Snapshot = WorldStateHash;
fn snapshot(&self) -> Self::Snapshot {
self.snapshot.clone()
}
fn matches_legacy_fragment(role: &str, text: &str) -> bool {
role == "developer" && PermissionsInstructions::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;
}
Some(Box::new(self.instructions.clone()))
}
}
#[cfg(test)]
#[path = "permissions_tests.rs"]
mod tests;
@@ -0,0 +1,94 @@
use super::*;
use crate::context::world_state::test_support::render_section_cases;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ApprovalMessages;
use codex_protocol::openai_models::PermissionMessages;
use codex_protocol::protocol::AskForApproval;
use pretty_assertions::assert_eq;
use std::path::Path;
#[test]
fn snapshots() {
use PreviousSectionState::Absent;
use PreviousSectionState::Known;
use PreviousSectionState::Unknown;
let read_only = permissions_state(PermissionProfile::read_only(), AskForApproval::OnRequest);
let full_access = permissions_state(PermissionProfile::Disabled, AskForApproval::OnRequest);
let never_ask = permissions_state(PermissionProfile::read_only(), AskForApproval::Never);
insta::assert_snapshot!(render_section_cases(&[
(Absent, Absent),
(Absent, Known(&read_only)),
(Known(&read_only), Known(&read_only)),
(Known(&read_only), Known(&full_access)),
(Known(&read_only), Known(&never_ask)),
(Unknown, Known(&read_only)),
]));
}
#[test]
fn persisted_permissions_are_detected_inside_bundled_developer_messages() {
let state = permissions_state(PermissionProfile::read_only(), AskForApproval::OnRequest);
let retained = ContextualUserFragment::into(state.instructions.clone());
let mut world_state = super::super::WorldState::default();
world_state.add_section(state);
let snapshot = world_state.snapshot();
let mut bundled_retained = retained.clone();
let ResponseItem::Message { content, .. } = &mut bundled_retained else {
panic!("permissions should render as a message");
};
content.insert(
0,
ContentItem::InputText {
text: "Other developer instructions.".to_string(),
},
);
assert_eq!(
world_state
.render_history_diff(/*previous*/ None, std::slice::from_ref(&retained))
.len(),
1,
);
assert_eq!(
world_state.render_history_diff(Some(&snapshot), &[]).len(),
1,
);
assert!(
world_state
.render_history_diff(Some(&snapshot), &[bundled_retained])
.is_empty()
);
}
fn permissions_state(
permission_profile: PermissionProfile,
approval_policy: AskForApproval,
) -> PermissionsState {
let approval_messages = ApprovalMessages {
on_request: Some("Ask for approval.".to_string()),
on_request_auto_review: None,
};
let permission_messages = PermissionMessages {
danger_full_access: Some("Full access.".to_string()),
workspace_write: Some("Workspace write.".to_string()),
read_only: Some("Read only.".to_string()),
};
PermissionsState::new(
&permission_profile,
approval_policy,
ApprovalPromptContext::new(
ApprovalsReviewer::User,
Some(&approval_messages),
Some(&permission_messages),
),
&Policy::empty(),
Path::new("/workspace"),
/*exec_permission_approvals_enabled*/ false,
/*request_permissions_tool_enabled*/ false,
)
}
@@ -0,0 +1,33 @@
---
source: core/src/context/world_state/permissions_tests.rs
expression: "render_section_cases(&[(Absent, Absent), (Absent, Known(&read_only)),\n(Known(&read_only), Known(&read_only)),\n(Known(&read_only), Known(&full_access)),\n(Known(&read_only), Known(&never_ask)), (Unknown, Known(&read_only)),])"
---
Absent -> Absent
None
Absent -> "0ccde536dd8b4cfebb1df573f679dfe86b5718e3" (role - developer)
<permissions instructions>
Read only.
Ask for approval.
</permissions instructions>
"0ccde536dd8b4cfebb1df573f679dfe86b5718e3" -> "0ccde536dd8b4cfebb1df573f679dfe86b5718e3"
None
"0ccde536dd8b4cfebb1df573f679dfe86b5718e3" -> "7e17d9a0df61e5f0c032ed7f0bf1dda58c9d5e85" (role - developer)
<permissions instructions>
Full access.
Ask for approval.
</permissions instructions>
"0ccde536dd8b4cfebb1df573f679dfe86b5718e3" -> "d96f228104cb899a8d3788b08e1d64b3bbb6ac84" (role - developer)
<permissions instructions>
Read only.
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
</permissions instructions>
Unknown -> "0ccde536dd8b4cfebb1df573f679dfe86b5718e3" (role - developer)
<permissions instructions>
Read only.
Ask for approval.
</permissions instructions>
@@ -53,6 +53,14 @@ impl ContextualUserFragment for TestFragment {
}
}
#[test]
fn world_state_hash_normalizes_crlf_line_endings() {
assert_eq!(
WorldStateHash::from_fragment(&TestFragment("line one\r\nline two".to_string())),
WorldStateHash::from_fragment(&TestFragment("line one\nline two".to_string())),
);
}
struct DuplicateTestSection;
impl WorldStateSection for DuplicateTestSection {
@@ -1,13 +1,9 @@
use crate::context::ApprovalPromptContext;
use crate::context::ContextualUserFragment;
use crate::context::ModelSwitchInstructions;
use crate::context::MultiAgentModeInstructions;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::session::PreviousTurnSettings;
use crate::session::turn_context::TurnContext;
use codex_execpolicy::Policy;
use codex_features::Feature;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::config_types::Personality;
use codex_protocol::models::ContentItem;
@@ -15,52 +11,6 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::protocol::TurnContextItem;
fn build_permissions_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
exec_policy: &Policy,
) -> Option<String> {
if !next.config.include_permissions_instructions {
return None;
}
let prev = previous?;
if prev.permission_profile() == next.permission_profile()
&& prev.approval_policy == next.approval_policy.value()
&& prev.model == next.model_info.slug
{
return None;
}
Some(
PermissionsInstructions::from_permission_profile(
&next.permission_profile,
next.approval_policy.value(),
ApprovalPromptContext::new(
next.config.approvals_reviewer,
next.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.approvals.as_ref()),
next.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.permissions.as_ref()),
),
exec_policy,
#[allow(deprecated)]
&next.cwd,
next.config
.features
.enabled(Feature::ExecPermissionApprovals),
next.config
.features
.enabled(Feature::RequestPermissionsTool),
)
.render(),
)
}
fn build_multi_agent_mode_update_item(
previous: Option<&TurnContextItem>,
next: &TurnContext,
@@ -185,7 +135,6 @@ pub(crate) fn build_settings_update_items(
previous: Option<&TurnContextItem>,
previous_turn_settings: Option<&PreviousTurnSettings>,
next: &TurnContext,
exec_policy: &Policy,
personality_feature_enabled: bool,
) -> Vec<ResponseItem> {
// TODO(ccunningham): build_settings_update_items still does not cover every
@@ -196,7 +145,6 @@ pub(crate) fn build_settings_update_items(
// Keep model-switch instructions first so model-specific guidance is read before
// any other context diffs on this turn.
build_model_instructions_update_item(previous_turn_settings, next),
build_permissions_update_item(previous, next, exec_policy),
build_multi_agent_mode_update_item(previous, next),
build_personality_update_item(previous, next, personality_feature_enabled),
]
+1 -38
View File
@@ -22,13 +22,11 @@ use crate::build_available_skills;
use crate::compact;
use crate::config::ManagedFeatures;
use crate::config::resolve_tool_suggest_config_from_layer_stack;
use crate::context::ApprovalPromptContext;
use crate::context::ApprovedCommandPrefixSaved;
use crate::context::AvailableSkillsInstructions;
use crate::context::ContextualUserFragment;
use crate::context::MultiAgentModeInstructions;
use crate::context::NetworkRuleSaved;
use crate::context::PermissionsInstructions;
use crate::context::PersonalitySpecInstructions;
use crate::context::RecommendedPluginsInstructions;
use crate::context::world_state::WorldState;
@@ -1702,18 +1700,16 @@ impl Session {
) -> Vec<ResponseItem> {
// TODO: Make context updates a pure diff of persisted previous/current TurnContextItem
// state so replay/backtracking is deterministic. Runtime inputs that affect model-visible
// context (exec policy, feature gates, previous-turn bridge) should be persisted
// context (feature gates and the previous-turn bridge) should be persisted
// state or explicit non-state replay events.
let previous_turn_settings = {
let state = self.state.lock().await;
state.previous_turn_settings()
};
let exec_policy = self.services.exec_policy.current();
crate::context_manager::updates::build_settings_update_items(
reference_context_item,
previous_turn_settings.as_ref(),
current_context,
exec_policy.as_ref(),
self.features.enabled(Feature::Personality),
)
}
@@ -3213,39 +3209,6 @@ impl Session {
{
developer_sections.push(model_switch_message);
}
if turn_context.config.include_permissions_instructions {
developer_sections.push(
PermissionsInstructions::from_permission_profile(
&turn_context.permission_profile,
turn_context.approval_policy.value(),
ApprovalPromptContext::new(
turn_context.config.approvals_reviewer,
turn_context
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.approvals.as_ref()),
turn_context
.model_info
.model_messages
.as_ref()
.and_then(|messages| messages.permissions.as_ref()),
),
self.services.exec_policy.current().as_ref(),
#[allow(deprecated)]
&turn_context.cwd,
turn_context
.config
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.config
.features
.enabled(Feature::RequestPermissionsTool),
)
.render(),
);
}
let separate_guardian_developer_message =
crate::guardian::is_guardian_reviewer_source(&session_source);
// Keep the guardian policy prompt out of the aggregated developer bundle so it
@@ -139,12 +139,21 @@ async fn record_initial_history_restores_world_state_baseline() {
let (session, turn_context) = make_session_and_context().await;
let turn_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &turn_context).await;
let rollout_items = completed_user_turn_rollout(
turn_context.to_turn_context_item(),
vec![RolloutItem::WorldState(WorldStateItem::full(
world_state.snapshot().into_value(),
))],
);
let expected_history = world_state
.render_full()
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
let mut world_state_items = expected_history
.iter()
.cloned()
.map(RolloutItem::ResponseItem)
.collect::<Vec<_>>();
world_state_items.push(RolloutItem::WorldState(WorldStateItem::full(
world_state.snapshot().into_value(),
)));
let rollout_items =
completed_user_turn_rollout(turn_context.to_turn_context_item(), world_state_items);
session
.record_initial_history(InitialHistory::Resumed(ResumedHistory {
@@ -158,7 +167,10 @@ async fn record_initial_history_restores_world_state_baseline() {
.record_context_updates_and_set_reference_context_item(&step_context)
.await;
assert_eq!(session.clone_history().await.raw_items(), &[]);
assert_eq!(
session.clone_history().await.raw_items(),
expected_history.as_slice(),
);
}
#[tokio::test]
@@ -8,6 +8,7 @@ Scenario: First request after fork when startup preserves the parent baseline, t
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:fork seed
03:message/developer:<PERMISSIONS_INSTRUCTIONS>
04:message/developer:<collaboration_mode>Fork turn collaboration instructions.</collaboration_mode>
05:message/user:after fork
03:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <collaboration_mode>Fork turn collaboration instructions.</collaboration_mode>
04:message/user:after fork
+12 -2
View File
@@ -8913,12 +8913,22 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
let previous_context_item = turn_context.to_turn_context_item();
let previous_context = Arc::new(turn_context);
let world_state = build_world_state_from_turn_context(&session, &previous_context).await;
let retained_world_state = world_state
.render_full()
.into_iter()
.map(ContextualUserFragment::into_boxed_response_item)
.collect::<Vec<_>>();
session
.replace_history(
retained_world_state.clone(),
Some(previous_context_item.clone()),
)
.await;
let mut turn_context = Arc::try_unwrap(previous_context)
.unwrap_or_else(|_| panic!("previous turn context should have no remaining references"));
turn_context.sub_id = format!("{}-next", turn_context.sub_id);
{
let mut state = session.state.lock().await;
state.set_reference_context_item(Some(previous_context_item.clone()));
state
.history
.set_world_state_baseline(world_state.snapshot());
@@ -8938,7 +8948,7 @@ async fn record_context_updates_and_set_reference_context_item_persists_baseline
assert_eq!(
session.clone_history().await.raw_items().to_vec(),
Vec::new()
retained_world_state
);
assert_eq!(
serde_json::to_value(session.reference_context_item().await)
+27
View File
@@ -1,11 +1,13 @@
use super::session::Session;
use super::step_context::StepContext;
use crate::connectors;
use crate::context::ApprovalPromptContext;
use crate::context::world_state::AgentsMdState;
use crate::context::world_state::AppsInstructionsState;
use crate::context::world_state::CollaborationModeState;
use crate::context::world_state::EnvironmentsInstructionsState;
use crate::context::world_state::EnvironmentsState;
use crate::context::world_state::PermissionsState;
use crate::context::world_state::PluginsInstructionsState;
use crate::context::world_state::RealtimeState;
use crate::context::world_state::WorldState;
@@ -40,6 +42,31 @@ impl Session {
.as_deref(),
));
world_state.add_section(AgentsMdState::new(step_context.loaded_agents_md.as_deref()));
if turn_context.config.include_permissions_instructions {
let permission_profile = turn_context.permission_profile();
let model_messages = turn_context.model_info.model_messages.as_ref();
let exec_policy = self.services.exec_policy.current();
world_state.add_section(PermissionsState::new(
&permission_profile,
turn_context.approval_policy.value(),
ApprovalPromptContext::new(
turn_context.config.approvals_reviewer,
model_messages.and_then(|messages| messages.approvals.as_ref()),
model_messages.and_then(|messages| messages.permissions.as_ref()),
),
exec_policy.as_ref(),
#[allow(deprecated)]
&turn_context.cwd,
turn_context
.config
.features
.enabled(Feature::ExecPermissionApprovals),
turn_context
.config
.features
.enabled(Feature::RequestPermissionsTool),
));
}
if turn_context.config.include_collaboration_mode_instructions
&& let Some(collaboration_mode) =
CollaborationModeState::from_collaboration_mode(&turn_context.collaboration_mode())
+21 -20
View File
@@ -1807,12 +1807,18 @@ async fn includes_user_instructions_message_in_request() {
.contains("be nice")
);
assert_message_role(&request_body["input"][0], "developer");
let permissions_text = request_body["input"][0]["content"][0]["text"]
.as_str()
.expect("invalid permissions message content");
let developer_texts = request_body["input"]
.as_array()
.expect("input array")
.iter()
.filter(|item| item.get("role").and_then(|role| role.as_str()) == Some("developer"))
.flat_map(message_input_texts)
.collect::<Vec<_>>();
assert!(
permissions_text.contains("`sandbox_mode`"),
"expected permissions message to mention sandbox_mode, got {permissions_text:?}"
developer_texts
.iter()
.any(|text| text.contains("`sandbox_mode`")),
"expected permissions message to mention sandbox_mode, got {developer_texts:?}"
);
assert_message_role(&request_body["input"][1], "user");
@@ -3095,10 +3101,6 @@ async fn includes_developer_instructions_message_in_request() {
let request = resp_mock.single_request();
let request_body = request.body_json();
let permissions_text = request_body["input"][0]["content"][0]["text"]
.as_str()
.expect("invalid permissions message content");
assert!(
!request_body["instructions"]
.as_str()
@@ -3106,23 +3108,22 @@ async fn includes_developer_instructions_message_in_request() {
.contains("be nice")
);
assert_message_role(&request_body["input"][0], "developer");
assert!(
permissions_text.contains("`sandbox_mode`"),
"expected permissions message to mention sandbox_mode, got {permissions_text:?}"
);
let developer_messages: Vec<&serde_json::Value> = request_body["input"]
let developer_texts = request_body["input"]
.as_array()
.expect("input array")
.iter()
.filter(|item| item.get("role").and_then(|role| role.as_str()) == Some("developer"))
.collect();
.flat_map(message_input_texts)
.collect::<Vec<_>>();
assert!(
developer_messages
developer_texts
.iter()
.any(|item| message_input_texts(item).contains(&"be useful")),
"expected developer instructions in a developer message, got {:?}",
request_body["input"]
.any(|text| text.contains("`sandbox_mode`")),
"expected permissions message to mention sandbox_mode, got {developer_texts:?}"
);
assert!(
developer_texts.contains(&"be useful"),
"expected developer instructions in a developer message, got {developer_texts:?}"
);
assert_message_role(&request_body["input"][1], "user");
+7 -5
View File
@@ -1233,18 +1233,20 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() {
return None;
}
let text = value
let texts = value
.get("content")
.and_then(|content| content.as_array())
.and_then(|content| content.first())
.and_then(|item| item.get("text"))
.and_then(|text| text.as_str());
.into_iter()
.flatten()
.filter_map(|item| item.get("text").and_then(|text| text.as_str()));
// Ignore cached prefix messages (project docs + permissions) since they are not
// relevant to compaction behavior and can change as bundled prompts evolve.
let role = value.get("role").and_then(|role| role.as_str());
if role == Some("developer")
&& text.is_some_and(|text| text.contains("`sandbox_mode`"))
&& texts
.into_iter()
.any(|text| text.contains("`sandbox_mode`"))
{
return None;
}
+26 -20
View File
@@ -660,16 +660,10 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
let permissions_texts: Vec<&str> = input
.iter()
.filter_map(|msg| {
let role = msg["role"].as_str()?;
if role != "developer" {
return None;
}
msg["content"]
.as_array()
.and_then(|content| content.first())
.and_then(|item| item["text"].as_str())
})
.filter(|msg| msg["role"].as_str() == Some("developer"))
.filter_map(|msg| msg["content"].as_array())
.flatten()
.filter_map(|item| item["text"].as_str())
.collect();
assert!(
permissions_texts.iter().any(|text| {
@@ -798,10 +792,6 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let expected_permissions_msg = body1["input"][0].clone();
let body1_input = body1["input"].as_array().expect("input array");
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
assert_ne!(
expected_settings_update_msg, expected_permissions_msg,
"expected updated permissions message after per-turn override"
);
assert_eq!(
expected_settings_update_msg["role"].as_str(),
Some("developer")
@@ -812,7 +802,16 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
}),
"expected model switch section after model override: {expected_settings_update_msg:?}"
);
let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone();
let expected_permissions_update_msg = body2["input"][body1_input.len() + 1].clone();
assert_ne!(
expected_permissions_update_msg, expected_permissions_msg,
"expected updated permissions message after per-turn override"
);
assert_eq!(
expected_permissions_update_msg["role"].as_str(),
Some("developer")
);
let expected_env_msg_2 = body2["input"][body1_input.len() + 2].clone();
assert_eq!(expected_env_msg_2["role"].as_str(), Some("user"));
let env_text = expected_env_msg_2["content"][0]["text"]
.as_str()
@@ -821,6 +820,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
assert_default_env_context(env_text, &expected_cwd);
let mut expected_body2 = body1_input.to_vec();
expected_body2.push(expected_settings_update_msg);
expected_body2.push(expected_permissions_update_msg);
expected_body2.push(expected_env_msg_2);
expected_body2.push(expected_user_message_2);
assert_eq_without_metadata(
@@ -1098,10 +1098,6 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
let body1_input = body1["input"].as_array().expect("input array");
let expected_settings_update_msg = body2["input"][body1_input.len()].clone();
assert_ne!(
expected_settings_update_msg, expected_permissions_msg,
"expected updated permissions message after policy change"
);
assert_eq!(
expected_settings_update_msg["role"].as_str(),
Some("developer")
@@ -1112,7 +1108,16 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
}),
"expected model switch section after model override: {expected_settings_update_msg:?}"
);
let expected_env_update_msg = body2["input"][body1_input.len() + 1].clone();
let expected_permissions_update_msg = body2["input"][body1_input.len() + 1].clone();
assert_ne!(
expected_permissions_update_msg, expected_permissions_msg,
"expected updated permissions message after policy change"
);
assert_eq!(
expected_permissions_update_msg["role"].as_str(),
Some("developer")
);
let expected_env_update_msg = body2["input"][body1_input.len() + 2].clone();
assert_eq!(expected_env_update_msg["role"].as_str(), Some("user"));
let expected_env_update_text = expected_env_update_msg["content"][0]["text"]
.as_str()
@@ -1130,6 +1135,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
expected_contextual_user_msg_1,
expected_user_message_1,
expected_settings_update_msg,
expected_permissions_update_msg,
expected_env_update_msg,
expected_user_message_2,
]);
@@ -6,8 +6,8 @@ Scenario: Remote manual /compact while realtime remains active: the next regular
## Remote Compaction Request
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
@@ -15,7 +15,7 @@ Scenario: Remote manual /compact while realtime remains active: the next regular
## Remote Post-Compaction History Layout
00:compaction:encrypted=true
01:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:USER_TWO
@@ -6,8 +6,8 @@ Scenario: Remote mid-turn continuation compaction after realtime was closed befo
## Second Turn Initial Request
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:SETUP_USER
03:message/assistant:REMOTE_SETUP_REPLY
@@ -16,8 +16,8 @@ Scenario: Remote mid-turn continuation compaction after realtime was closed befo
## Remote Compaction Request
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:SETUP_USER
03:message/assistant:REMOTE_SETUP_REPLY
@@ -6,8 +6,8 @@ Scenario: Remote pre-turn auto-compaction while realtime remains active: compact
## Remote Compaction Request
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:USER_ONE
03:message/assistant:REMOTE_FIRST_REPLY
@@ -15,7 +15,7 @@ Scenario: Remote pre-turn auto-compaction while realtime remains active: compact
## Remote Post-Compaction History Layout
00:compaction:encrypted=true
01:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[01] <realtime_conversation>\nRealtime conversation started.\n\nYou a...
[02] <PERMISSIONS_INSTRUCTIONS>
02:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
03:message/user:USER_TWO
@@ -6,8 +6,8 @@ Scenario: Second turn changes cwd to a directory with different AGENTS.md and re
## First Request (agents_one)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
@@ -15,8 +15,8 @@ Scenario: Second turn changes cwd to a directory with different AGENTS.md and re
## Second Request (agents_two cwd)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user[2]:
[01] <AGENTS_MD>
[02] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
@@ -6,15 +6,15 @@ Scenario: First post-resume turn where pre-turn override sets model to rollout m
## Last Request Before Resume
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:seed resume history
## First Request After Resume + Override
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:seed resume history
03:message/assistant:recorded before resume
@@ -6,20 +6,19 @@ Scenario: First post-resume turn where resumed config model differs from rollout
## Last Request Before Resume
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:seed resume history
## First Request After Resume
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:seed resume history
03:message/assistant:recorded before resume
04:message/developer[2]:
[01] <model_switch>\nThe user was previously using a different model. Please continue the conversatio...
[02] <PERMISSIONS_INSTRUCTIONS>
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
06:message/user:resume and change personality
04:message/developer:<model_switch>\nThe user was previously using a different model. Please continue the conversatio...
05:message/developer:<PERMISSIONS_INSTRUCTIONS>
06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
07:message/user:resume and change personality
@@ -6,20 +6,19 @@ Scenario: Second turn changes cwd, approval policy, and personality while keepin
## First Request (Baseline)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:first turn
## Second Request (Turn Overrides)
00:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[01] <SKILLS_INSTRUCTIONS>
[02] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:first turn
03:message/assistant:turn one complete
04:message/developer[2]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <personality_spec> The user has requested a new communication style. Future messages should adhe...
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
06:message/user:second turn with context updates
04:message/developer:<personality_spec> The user has requested a new communication style. Future messages should adhe...
05:message/developer:<PERMISSIONS_INSTRUCTIONS>
06:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
07:message/user:second turn with context updates
@@ -1,15 +1,14 @@
---
source: core/tests/suite/token_budget.rs
assertion_line: 545
expression: snapshot
---
Scenario: New context window tool installs fresh full context before the next follow-up request.
## Final Follow-Up Request
00:message/developer[3]:
[01] <PERMISSIONS_INSTRUCTIONS>
[02] <SKILLS_INSTRUCTIONS>
[03] <context_window>\nThread id: <THREAD_ID>\nFirst context window id: <FIRST_WINDOW_ID>\nCurrent context window id: <WINDOW_ID>\nPrevious context window id: <FIRST_WINDOW_ID>\n</context_window>
[01] <SKILLS_INSTRUCTIONS>
[02] <context_window>\nThread id: <THREAD_ID>\nFirst context window id: <FIRST_WINDOW_ID>\nCurrent context window id: <WINDOW_ID>\nPrevious context window id: <FIRST_WINDOW_ID>\n</context_window>
[03] <PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:function_call/update_plan
03:function_call_output:Plan updated