mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Stop TUI chats on misalignment policy violations (#39261)
## Why When the app server reports a misalignment policy violation, the TUI must stop the affected chat rather than allow more work to continue in it. ## What changed - Finalize the active turn, clear queued and draft input, disable the composer, and reject further operations and interactive requests for the stopped chat. - Interrupt an active side conversation when its parent reports the violation, and prevent queued turns, forks, approvals, or goal resumption from restarting work. - Show a non-dismissible precaution view that directs users to start a new chat or resume another one, with agent command center navigation for remote sessions. ## Testing Added coverage for stopped-chat behavior, parent/side-thread handling, blocked goal resumption, navigation actions, and narrow and remote-session rendering. GitOrigin-RevId: 0268c46a19294a17765ea4099f389f15ffaaf030
This commit is contained in:
@@ -13,6 +13,7 @@ use crate::config_update::format_config_error;
|
||||
use crate::external_agent_config_migration::flow::ExternalAgentConfigMigrationFlowOutcome;
|
||||
use crate::pager_overlay::TranscriptHistoryState;
|
||||
use crate::session_resume::cwds_differ;
|
||||
use codex_app_server_protocol::ThreadGoalStatus;
|
||||
#[cfg(target_os = "windows")]
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
|
||||
@@ -25,6 +26,24 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
event: AppEvent,
|
||||
) -> Result<AppRunControl> {
|
||||
if self.chat_widget.has_misalignment_policy_violation()
|
||||
&& matches!(
|
||||
event,
|
||||
AppEvent::OpenAgentPicker
|
||||
| AppEvent::SelectAgentThread(_)
|
||||
| AppEvent::StartSide { .. }
|
||||
| AppEvent::ForkCurrentSession { .. }
|
||||
| AppEvent::ForkSessionForPromptEdit { .. }
|
||||
| AppEvent::SetThreadGoalDraft { .. }
|
||||
| AppEvent::SetThreadGoalStatus {
|
||||
status: ThreadGoalStatus::Active,
|
||||
..
|
||||
}
|
||||
)
|
||||
{
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
match event {
|
||||
AppEvent::SkillsListLoaded { ref cwd, .. }
|
||||
| AppEvent::PluginMentionsLoaded { ref cwd, .. }
|
||||
@@ -35,6 +54,9 @@ impl App {
|
||||
/*initial_user_message*/ None, name,
|
||||
)
|
||||
.await;
|
||||
if self.chat_widget.has_misalignment_policy_violation() {
|
||||
self.chat_widget.show_misalignment_policy_precaution();
|
||||
}
|
||||
}
|
||||
AppEvent::ChangeWorkingDirectory {
|
||||
thread_id,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use super::*;
|
||||
use crate::chatwidget::UserMessage;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::ErrorNotification;
|
||||
use codex_app_server_protocol::ThreadGoalStatus;
|
||||
use codex_protocol::models::ManagedFileSystemPermissions;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_failure_is_shown_without_exiting() -> Result<()> {
|
||||
@@ -57,6 +61,142 @@ async fn turn_start_failure_is_shown_without_exiting() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_policy_blocks_queued_turns_and_goal_resumption() -> Result<()> {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
|
||||
let started = app_server.start_thread(&app.config).await?;
|
||||
let thread_id = started.session.thread_id;
|
||||
let session = started.session.clone();
|
||||
app.enqueue_primary_thread_session(started.session, started.turns)
|
||||
.await?;
|
||||
app_server
|
||||
.thread_goal_set(
|
||||
thread_id,
|
||||
Some("Do not restart this stopped goal".to_string()),
|
||||
Some(ThreadGoalStatus::Blocked),
|
||||
/*token_budget*/ None,
|
||||
)
|
||||
.await?;
|
||||
app.chat_widget
|
||||
.restore_user_message_to_composer("queued before terminal failure".into());
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
let queued_turn = next_user_turn_op(&mut op_rx);
|
||||
let failed_turn = Turn {
|
||||
error: Some(AppServerTurnError {
|
||||
message: "misalignment policy violation".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
}),
|
||||
..test_turn("turn-1", TurnStatus::Failed, Vec::new())
|
||||
};
|
||||
app.enqueue_primary_thread_session(session, vec![failed_turn])
|
||||
.await?;
|
||||
let precaution = render_bottom_popup(&app.chat_widget, /*width*/ 80);
|
||||
let AppServerRequestId::Integer(next_request_id) = app_server.next_request_id() else {
|
||||
unreachable!("embedded app-server request IDs are integers");
|
||||
};
|
||||
|
||||
app.maybe_prompt_resume_paused_goal_after_resume(&mut app_server, thread_id)
|
||||
.await;
|
||||
for event in [
|
||||
AppEvent::CodexOp(queued_turn),
|
||||
AppEvent::CodexOp(crate::app_command::AppCommand::ExecApproval {
|
||||
id: "queued-approval".to_string(),
|
||||
turn_id: None,
|
||||
decision: codex_app_server_protocol::CommandExecutionApprovalDecision::Accept,
|
||||
}),
|
||||
AppEvent::SetThreadGoalStatus {
|
||||
thread_id,
|
||||
status: ThreadGoalStatus::Active,
|
||||
},
|
||||
AppEvent::SetThreadGoalDraft {
|
||||
thread_id,
|
||||
draft: crate::goal_files::GoalDraft {
|
||||
objective: "Do not replace this stopped goal".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
mode: crate::app_event::ThreadGoalSetMode::ReplaceExisting,
|
||||
},
|
||||
AppEvent::StartSide {
|
||||
parent_thread_id: thread_id,
|
||||
user_message: Some("Do not fork this stopped thread".into()),
|
||||
},
|
||||
AppEvent::ForkCurrentSession { name: None },
|
||||
AppEvent::OpenAgentPicker,
|
||||
AppEvent::SelectAgentThread(thread_id),
|
||||
] {
|
||||
app.handle_event(&mut tui, &mut app_server, event).await?;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
app_server.next_request_id(),
|
||||
AppServerRequestId::Integer(next_request_id + 1)
|
||||
);
|
||||
assert_eq!(
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 80),
|
||||
precaution
|
||||
);
|
||||
assert_eq!(
|
||||
app_server
|
||||
.thread_goal_get(thread_id)
|
||||
.await?
|
||||
.goal
|
||||
.unwrap()
|
||||
.status,
|
||||
ThreadGoalStatus::Blocked
|
||||
);
|
||||
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_policy_in_parent_stops_active_side_conversation() -> Result<()> {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
let side_thread_id = ThreadId::new();
|
||||
app.primary_thread_id = Some(parent_thread_id);
|
||||
app.active_thread_id = Some(side_thread_id);
|
||||
app.side_threads
|
||||
.insert(side_thread_id, SideThreadState::new(parent_thread_id));
|
||||
app.chat_widget.handle_thread_session(test_thread_session(
|
||||
side_thread_id,
|
||||
app.config.cwd.to_path_buf(),
|
||||
));
|
||||
app.chat_widget
|
||||
.restore_user_message_to_composer("pending side turn".into());
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
|
||||
app.enqueue_thread_notification(
|
||||
parent_thread_id,
|
||||
ServerNotification::Error(ErrorNotification {
|
||||
error: AppServerTurnError {
|
||||
message: "misalignment policy violation".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
},
|
||||
will_retry: false,
|
||||
thread_id: parent_thread_id.to_string(),
|
||||
turn_id: "parent-turn".to_string(),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(app.chat_widget.has_misalignment_policy_violation());
|
||||
assert!(
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 80)
|
||||
.contains("Chat stopped as a precaution")
|
||||
);
|
||||
assert_matches!(op_rx.try_recv(), Ok(Op::Interrupt));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unsupported_legacy_permissions_are_shown_without_exiting() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
@@ -56,6 +56,10 @@ impl App {
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
) {
|
||||
if self.chat_widget.has_misalignment_policy_violation() {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = app_server.thread_goal_get(thread_id).await;
|
||||
if self.current_displayed_thread_id() != Some(thread_id) {
|
||||
return;
|
||||
|
||||
@@ -346,6 +346,10 @@ impl App {
|
||||
}
|
||||
|
||||
pub(super) fn push_thread_interactive_request(&mut self, request: ThreadInteractiveRequest) {
|
||||
if self.chat_widget.has_misalignment_policy_violation() {
|
||||
return;
|
||||
}
|
||||
|
||||
match request {
|
||||
ThreadInteractiveRequest::AppLink(params) => {
|
||||
self.chat_widget.open_app_link_view(params);
|
||||
@@ -444,6 +448,10 @@ impl App {
|
||||
thread_id: ThreadId,
|
||||
op: AppCommand,
|
||||
) -> Result<()> {
|
||||
if self.chat_widget.rejects_misalignment_policy_op(&op) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
crate::session_log::log_outbound_op(&op);
|
||||
|
||||
if self
|
||||
@@ -960,6 +968,24 @@ impl App {
|
||||
if self.abandoned_side_threads.contains(&thread_id) {
|
||||
return Ok(());
|
||||
}
|
||||
let misalignment_policy_violation =
|
||||
match ¬ification {
|
||||
ServerNotification::Error(notification) if !notification.will_retry => {
|
||||
notification.error.codex_error_info.as_ref()
|
||||
}
|
||||
ServerNotification::TurnCompleted(notification) => notification
|
||||
.turn
|
||||
.error
|
||||
.as_ref()
|
||||
.and_then(|error| error.codex_error_info.as_ref()),
|
||||
_ => None,
|
||||
} == Some(&AppServerCodexErrorInfo::MisalignmentPolicyViolation);
|
||||
if misalignment_policy_violation && self.active_side_parent_thread_id() == Some(thread_id) {
|
||||
if self.chat_widget.is_user_turn_pending_or_running() {
|
||||
self.chat_widget.submit_op(AppCommand::Interrupt);
|
||||
}
|
||||
self.chat_widget.on_misalignment_policy_violation();
|
||||
}
|
||||
if matches!(
|
||||
notification,
|
||||
ServerNotification::ThreadSettingsUpdated(_) | ServerNotification::ThreadArchived(_)
|
||||
|
||||
@@ -364,6 +364,7 @@ use self::interrupts::InterruptManager;
|
||||
mod keymap_picker;
|
||||
mod mcp_startup;
|
||||
use self::mcp_startup::McpStartupStatus;
|
||||
mod misalignment_policy;
|
||||
mod pets;
|
||||
mod session_flow;
|
||||
mod session_header;
|
||||
@@ -674,6 +675,7 @@ pub(crate) struct ChatWidget {
|
||||
thread_rename_block_message: Option<String>,
|
||||
active_side_conversation: bool,
|
||||
blocks_direct_input: bool,
|
||||
misalignment_policy_violation: bool,
|
||||
normal_placeholder_text: String,
|
||||
side_placeholder_text: String,
|
||||
forked_from: Option<ThreadId>,
|
||||
@@ -1790,6 +1792,9 @@ impl ChatWidget {
|
||||
T: Into<AppCommand>,
|
||||
{
|
||||
let op: AppCommand = op.into();
|
||||
if self.rejects_misalignment_policy_op(&op) {
|
||||
return false;
|
||||
}
|
||||
if self.blocks_direct_input
|
||||
&& matches!(
|
||||
&op,
|
||||
|
||||
@@ -203,6 +203,7 @@ impl ChatWidget {
|
||||
thread_rename_block_message: None,
|
||||
active_side_conversation: false,
|
||||
blocks_direct_input: false,
|
||||
misalignment_policy_violation: false,
|
||||
normal_placeholder_text: placeholder,
|
||||
side_placeholder_text: side_placeholder,
|
||||
forked_from: None,
|
||||
|
||||
@@ -110,6 +110,9 @@ impl ChatWidget {
|
||||
action: QueuedInputAction,
|
||||
pending_pastes: Vec<(String, String)>,
|
||||
) {
|
||||
if self.misalignment_policy_violation {
|
||||
return;
|
||||
}
|
||||
let should_run_now = self.is_session_configured()
|
||||
&& !self.is_user_turn_pending_or_running()
|
||||
&& !self.input_queue.suppress_queue_autosend;
|
||||
@@ -135,7 +138,7 @@ impl ChatWidget {
|
||||
|
||||
/// If idle and there are queued inputs, submit exactly one to start the next turn.
|
||||
pub(crate) fn maybe_send_next_queued_input(&mut self) -> bool {
|
||||
if self.input_queue.suppress_queue_autosend {
|
||||
if self.misalignment_policy_violation || self.input_queue.suppress_queue_autosend {
|
||||
return false;
|
||||
}
|
||||
if self.blocks_direct_input {
|
||||
@@ -178,7 +181,7 @@ impl ChatWidget {
|
||||
submitted_follow_up
|
||||
}
|
||||
|
||||
pub(super) fn is_user_turn_pending_or_running(&self) -> bool {
|
||||
pub(crate) fn is_user_turn_pending_or_running(&self) -> bool {
|
||||
self.input_queue.user_turn_pending_start
|
||||
|| self.turn_lifecycle.agent_turn_running
|
||||
|| self.review.is_review_mode
|
||||
|
||||
@@ -99,6 +99,9 @@ impl ChatWidget {
|
||||
history_record: UserMessageHistoryRecord,
|
||||
shell_escape_policy: ShellEscapePolicy,
|
||||
) -> (bool, Option<AppCommand>) {
|
||||
if self.misalignment_policy_violation {
|
||||
return (false, None);
|
||||
}
|
||||
if !self.is_session_configured() {
|
||||
tracing::warn!("cannot submit user message before session is configured; queueing");
|
||||
self.input_queue
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Terminal precaution UI for misalignment policy violations.
|
||||
|
||||
use super::*;
|
||||
|
||||
const MISALIGNMENT_POLICY_TITLE: &str = "Chat stopped as a precaution";
|
||||
const MISALIGNMENT_POLICY_DESCRIPTION: &str = "We couldn’t confirm the agent was acting safely and following your instructions. To continue working, start or resume another chat.";
|
||||
|
||||
impl ChatWidget {
|
||||
pub(crate) fn has_misalignment_policy_violation(&self) -> bool {
|
||||
self.misalignment_policy_violation
|
||||
}
|
||||
|
||||
pub(crate) fn rejects_misalignment_policy_op(&self, op: &AppCommand) -> bool {
|
||||
self.misalignment_policy_violation
|
||||
&& !matches!(
|
||||
op,
|
||||
AppCommand::Interrupt
|
||||
| AppCommand::CleanBackgroundTerminals
|
||||
| AppCommand::OverrideTurnContext { .. }
|
||||
| AppCommand::ReloadUserConfig
|
||||
| AppCommand::ListSkills { .. }
|
||||
| AppCommand::SetThreadName { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn on_misalignment_policy_violation(&mut self) {
|
||||
if self.misalignment_policy_violation {
|
||||
return;
|
||||
}
|
||||
|
||||
self.misalignment_policy_violation = true;
|
||||
self.input_queue.clear();
|
||||
self.finalize_turn();
|
||||
self.refresh_pending_input_preview();
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
self.bottom_pane
|
||||
.set_composer_text(String::new(), Vec::new(), Vec::new());
|
||||
self.bottom_pane.set_composer_input_enabled(
|
||||
/*enabled*/ false,
|
||||
Some(MISALIGNMENT_POLICY_TITLE.to_string()),
|
||||
);
|
||||
|
||||
self.show_misalignment_policy_precaution();
|
||||
}
|
||||
|
||||
pub(crate) fn show_misalignment_policy_precaution(&mut self) {
|
||||
let mut items = vec![
|
||||
SelectionItem {
|
||||
name: "New chat".to_string(),
|
||||
actions: vec![Box::new(|tx| tx.send(AppEvent::NewSession { name: None }))],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Resume another chat".to_string(),
|
||||
actions: vec![Box::new(|tx| tx.send(AppEvent::OpenResumePicker))],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
if self.remote_connection.is_some() {
|
||||
items.insert(
|
||||
1,
|
||||
SelectionItem {
|
||||
name: "Agent command center".to_string(),
|
||||
actions: vec![Box::new(|tx| tx.send(AppEvent::OpenAgentsOverview))],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
header: Box::new(
|
||||
Paragraph::new(vec![
|
||||
Line::from(MISALIGNMENT_POLICY_TITLE).bold(),
|
||||
Line::from(MISALIGNMENT_POLICY_DESCRIPTION).dim(),
|
||||
])
|
||||
.wrap(Wrap { trim: false }),
|
||||
),
|
||||
items,
|
||||
allow_cancel: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@ impl ChatWidget {
|
||||
request: ServerRequest,
|
||||
replay_kind: Option<ReplayKind>,
|
||||
) {
|
||||
if self.misalignment_policy_violation {
|
||||
return;
|
||||
}
|
||||
|
||||
let id = request.id().to_string();
|
||||
match request {
|
||||
ServerRequest::CommandExecutionRequestApproval { params, .. } => {
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/misalignment_policy_tests.rs
|
||||
expression: popup
|
||||
---
|
||||
Chat stopped as a precaution
|
||||
We couldn’t confirm the agent was acting safely and following your
|
||||
instructions. To continue working, start or resume another chat.
|
||||
|
||||
› 1. New chat
|
||||
2. Resume another chat
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/misalignment_policy_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 38)"
|
||||
---
|
||||
Chat stopped as a precaution
|
||||
We couldn’t confirm the agent was
|
||||
acting safely and following your
|
||||
instructions. To continue working,
|
||||
start or resume another chat.
|
||||
|
||||
› 1. New chat
|
||||
2. Resume another chat
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/misalignment_policy_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Chat stopped as a precaution
|
||||
We couldn’t confirm the agent was acting safely and following your
|
||||
instructions. To continue working, start or resume another chat.
|
||||
|
||||
› 1. New chat
|
||||
2. Agent command center
|
||||
3. Resume another chat
|
||||
@@ -243,6 +243,8 @@ mod guardian;
|
||||
pub(crate) mod helpers;
|
||||
mod history_replay;
|
||||
mod mcp_startup;
|
||||
#[path = "tests/misalignment_policy_tests.rs"]
|
||||
mod misalignment_policy;
|
||||
mod permissions;
|
||||
mod plan_mode;
|
||||
#[path = "tests/plugin_catalog_tests.rs"]
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const ERROR_MESSAGE: &str = "Responses API returned misalignment_policy_violation";
|
||||
|
||||
#[tokio::test]
|
||||
async fn misalignment_policy_failure_stops_the_thread_and_renders_once() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(thread_id);
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ModelSafetyBufferingUpdated(ModelSafetyBufferingUpdatedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
model: "current-model".to_string(),
|
||||
use_cases: Vec::new(),
|
||||
reasons: Vec::new(),
|
||||
show_buffering_ui: true,
|
||||
faster_model: None,
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
assert!(render_bottom_popup(&chat, /*width*/ 80).contains("thinking a bit more"));
|
||||
chat.queue_user_message(UserMessage::from("queued follow-up"));
|
||||
chat.bottom_pane
|
||||
.set_composer_text("stale draft".to_string(), Vec::new(), Vec::new());
|
||||
drain_insert_history(&mut rx);
|
||||
|
||||
handle_error(
|
||||
&mut chat,
|
||||
ERROR_MESSAGE,
|
||||
Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
);
|
||||
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
assert!(!chat.bottom_pane.composer_input_enabled());
|
||||
assert!(chat.queued_user_message_texts().is_empty());
|
||||
assert_eq!(chat.bottom_pane.composer_text_with_pending(), "");
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("misalignment_policy_violation", popup);
|
||||
assert_chatwidget_snapshot!(
|
||||
"misalignment_policy_violation_narrow",
|
||||
render_bottom_popup(&chat, /*width*/ 38)
|
||||
);
|
||||
|
||||
chat.submit_user_message(UserMessage::from("direct follow-up"));
|
||||
chat.queue_user_message(UserMessage::from("queued follow-up"));
|
||||
assert!(!chat.submit_op(Op::Compact));
|
||||
assert!(!chat.submit_op(Op::run_user_shell_command("echo unsafe".to_string())));
|
||||
assert!(!chat.maybe_send_next_queued_input());
|
||||
assert!(chat.queued_user_message_texts().is_empty());
|
||||
assert_no_submit_op(&mut op_rx);
|
||||
|
||||
chat.handle_server_request(
|
||||
ServerRequest::ToolRequestUserInput {
|
||||
request_id: AppServerRequestId::Integer(1),
|
||||
params: ToolRequestUserInputParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "input-1".to_string(),
|
||||
questions: Vec::new(),
|
||||
is_blocking: true,
|
||||
auto_resolution_ms: None,
|
||||
},
|
||||
},
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TurnCompleted(TurnCompletedNotification {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: app_server_turn(
|
||||
"turn-1",
|
||||
AppServerTurnStatus::Failed,
|
||||
/*duration_ms*/ None,
|
||||
Some(AppServerTurnError {
|
||||
message: ERROR_MESSAGE.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::MisalignmentPolicyViolation),
|
||||
additional_details: None,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), popup);
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NewSession { name: None }));
|
||||
assert!(rx.try_recv().is_err());
|
||||
|
||||
chat.show_misalignment_policy_precaution();
|
||||
assert_eq!(render_bottom_popup(&chat, /*width*/ 80), popup);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::NewSession { name: None }));
|
||||
|
||||
chat.show_misalignment_policy_precaution();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
|
||||
assert!(chat.bottom_pane.has_active_view());
|
||||
|
||||
chat.remote_connection = Some(crate::status::remote_connection::RemoteConnectionStatus {
|
||||
address: "wss://remote.example.com".to_string(),
|
||||
version: "v1.0.0".to_string(),
|
||||
});
|
||||
chat.show_misalignment_policy_precaution();
|
||||
assert_chatwidget_snapshot!(
|
||||
"misalignment_policy_violation_remote",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenAgentsOverview));
|
||||
assert!(chat.bottom_pane.has_active_view());
|
||||
}
|
||||
@@ -443,7 +443,9 @@ impl ChatWidget {
|
||||
message: String,
|
||||
codex_error_info: Option<AppServerCodexErrorInfo>,
|
||||
) {
|
||||
if codex_error_info
|
||||
if codex_error_info == Some(AppServerCodexErrorInfo::MisalignmentPolicyViolation) {
|
||||
self.on_misalignment_policy_violation();
|
||||
} else if codex_error_info
|
||||
.as_ref()
|
||||
.is_some_and(|info| self.handle_app_server_steer_rejected_error(info))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user