mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -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,
|
||||
)
|
||||
}
|
||||
+33
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
+4
-3
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+4
-4
@@ -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>>
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
+8
-9
@@ -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
|
||||
|
||||
+8
-9
@@ -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
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user