mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Enforce user input length cap (#12823)
Currently there is no bound on the length of a user message submitted in the TUI or through the app server interface. That means users can paste many megabytes of text, which can lead to bad performance, hangs, and crashes. In extreme cases, it can lead to a [kernel panic](https://github.com/openai/codex/issues/12323). This PR limits the length of a user input to 2**20 (about 1M) characters. This value was chosen because it fills the entire context window on the latest models, so accepting longer inputs wouldn't make sense anyway. Summary - add a shared `MAX_USER_INPUT_TEXT_CHARS` constant in codex-protocol and surface it in TUI and app server code - block oversized submissions in the TUI submit flow and emit error history cells when validation fails - reject heavy app-server requests with JSON-RPC `-32602` and structured `input_too_large` data, plus document the behavior Testing - ran the IDE extension with this change and verified that when I attempt to paste a user message that's several MB long, it correctly reports an error instead of crashing or making my computer hot.
This commit is contained in:
@@ -189,6 +189,7 @@ use codex_protocol::custom_prompts::CustomPrompt;
|
||||
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;
|
||||
use codex_protocol::models::local_image_label_text;
|
||||
use codex_protocol::user_input::ByteRange;
|
||||
use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use codex_utils_fuzzy_match::fuzzy_match;
|
||||
|
||||
@@ -229,6 +230,12 @@ use tokio::runtime::Handle;
|
||||
/// placeholder in the UI.
|
||||
const LARGE_PASTE_CHAR_THRESHOLD: usize = 1000;
|
||||
|
||||
fn user_input_too_large_message(actual_chars: usize) -> String {
|
||||
format!(
|
||||
"Message exceeds the maximum length of {MAX_USER_INPUT_TEXT_CHARS} characters ({actual_chars} provided)."
|
||||
)
|
||||
}
|
||||
|
||||
/// Result returned when the user interacts with the text area.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum InputResult {
|
||||
@@ -570,6 +577,10 @@ impl ChatComposer {
|
||||
self.realtime_conversation_enabled = enabled;
|
||||
}
|
||||
|
||||
/// Compatibility shim for tests that still toggle the removed steer mode flag.
|
||||
#[cfg(test)]
|
||||
pub fn set_steer_enabled(&mut self, _enabled: bool) {}
|
||||
|
||||
pub fn set_voice_transcription_enabled(&mut self, enabled: bool) {
|
||||
self.voice_state.transcription_enabled = enabled;
|
||||
if !enabled {
|
||||
@@ -2309,6 +2320,22 @@ impl ChatComposer {
|
||||
text_elements = expanded.text_elements;
|
||||
}
|
||||
}
|
||||
let actual_chars = text.chars().count();
|
||||
if actual_chars > MAX_USER_INPUT_TEXT_CHARS {
|
||||
let message = user_input_too_large_message(actual_chars);
|
||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_error_event(message),
|
||||
)));
|
||||
self.set_text_content_with_mention_bindings(
|
||||
original_input.clone(),
|
||||
original_text_elements,
|
||||
original_local_image_paths,
|
||||
original_mention_bindings,
|
||||
);
|
||||
self.pending_pastes.clone_from(&original_pending_pastes);
|
||||
self.textarea.set_cursor(original_input.len());
|
||||
return None;
|
||||
}
|
||||
// Custom prompt expansion can remove or rewrite image placeholders, so prune any
|
||||
// attachments that no longer have a corresponding placeholder in the expanded text.
|
||||
self.prune_attached_images_for_submission(&text, &text_elements);
|
||||
@@ -5921,6 +5948,118 @@ mod tests {
|
||||
assert!(composer.pending_pastes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_at_character_limit_succeeds() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS);
|
||||
composer.textarea.set_text_clearing_elements(&input);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
InputResult::Submitted { text, .. } if text == input
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_submit_reports_error_and_restores_draft() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1);
|
||||
composer.textarea.set_text_clearing_elements(&input);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(InputResult::None, result);
|
||||
assert_eq!(composer.textarea.text(), input);
|
||||
|
||||
let mut found_error = false;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
let message = cell
|
||||
.display_lines(80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(message.contains(&user_input_too_large_message(input.chars().count())));
|
||||
found_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(found_error, "expected oversized-input error history cell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_queued_submission_reports_error_and_restores_draft() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(false);
|
||||
let input = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1);
|
||||
composer.textarea.set_text_clearing_elements(&input);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(InputResult::None, result);
|
||||
assert_eq!(composer.textarea.text(), input);
|
||||
|
||||
let mut found_error = false;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
let message = cell
|
||||
.display_lines(80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(message.contains(&user_input_too_large_message(input.chars().count())));
|
||||
found_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(found_error, "expected oversized-input error history cell");
|
||||
}
|
||||
|
||||
/// Behavior: editing that removes a paste placeholder should also clear the associated
|
||||
/// `pending_pastes` entry so it cannot be submitted accidentally.
|
||||
#[test]
|
||||
@@ -8521,6 +8660,57 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_expansion_over_character_limit_reports_error_and_restores_draft() {
|
||||
let (tx, mut rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
true,
|
||||
sender,
|
||||
false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
false,
|
||||
);
|
||||
composer.set_steer_enabled(true);
|
||||
|
||||
composer.set_custom_prompts(vec![CustomPrompt {
|
||||
name: "my-prompt".to_string(),
|
||||
path: "/tmp/my-prompt.md".to_string().into(),
|
||||
content: "Echo: $1".to_string(),
|
||||
description: None,
|
||||
argument_hint: None,
|
||||
}]);
|
||||
|
||||
let oversized_arg = "x".repeat(MAX_USER_INPUT_TEXT_CHARS);
|
||||
let original_input = format!("/prompts:my-prompt {oversized_arg}");
|
||||
composer
|
||||
.textarea
|
||||
.set_text_clearing_elements(&original_input);
|
||||
|
||||
let (result, _needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_eq!(InputResult::None, result);
|
||||
assert_eq!(composer.textarea.text(), original_input);
|
||||
|
||||
let actual_chars = format!("Echo: {oversized_arg}").chars().count();
|
||||
let mut found_error = false;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
let message = cell
|
||||
.display_lines(80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(message.contains(&user_input_too_large_message(actual_chars)));
|
||||
found_error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(found_error, "expected oversized-input error history cell");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_custom_prompt_with_positional_args_submits_numeric_expansion() {
|
||||
let prompt_text = "Header: $1\nArgs: $ARGUMENTS\n";
|
||||
|
||||
@@ -2602,6 +2602,16 @@ mod tests {
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_event_oversized_input_snapshot() {
|
||||
let cell = new_error_event(
|
||||
"Message exceeds the maximum length of 1048576 characters (1048577 provided)."
|
||||
.to_string(),
|
||||
);
|
||||
let rendered = render_lines(&cell.display_lines(120)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tools_output_masks_sensitive_values() {
|
||||
let mut config = test_config().await;
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
expression: rendered
|
||||
---
|
||||
■ Message exceeds the maximum length of 1048576 characters (1048577 provided).
|
||||
Reference in New Issue
Block a user