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.
106 lines
3.5 KiB
Rust
106 lines
3.5 KiB
Rust
use schemars::JsonSchema;
|
||
use serde::Deserialize;
|
||
use serde::Serialize;
|
||
use ts_rs::TS;
|
||
|
||
/// Conservative cap so one user message cannot monopolize a large context window.
|
||
pub const MAX_USER_INPUT_TEXT_CHARS: usize = 1 << 20;
|
||
|
||
/// User input
|
||
#[non_exhaustive]
|
||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]
|
||
#[serde(tag = "type", rename_all = "snake_case")]
|
||
pub enum UserInput {
|
||
Text {
|
||
text: String,
|
||
/// UI-defined spans within `text` that should be treated as special elements.
|
||
/// These are byte ranges into the UTF-8 `text` buffer and are used to render
|
||
/// or persist rich input markers (e.g., image placeholders) across history
|
||
/// and resume without mutating the literal text.
|
||
#[serde(default)]
|
||
text_elements: Vec<TextElement>,
|
||
},
|
||
/// Pre‑encoded data: URI image.
|
||
Image { image_url: String },
|
||
|
||
/// Local image path provided by the user. This will be converted to an
|
||
/// `Image` variant (base64 data URL) during request serialization.
|
||
LocalImage { path: std::path::PathBuf },
|
||
|
||
/// Skill selected by the user (name + path to SKILL.md).
|
||
Skill {
|
||
name: String,
|
||
path: std::path::PathBuf,
|
||
},
|
||
/// Explicit mention selected by the user (name + app://connector id).
|
||
Mention { name: String, path: String },
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)]
|
||
pub struct TextElement {
|
||
/// Byte range in the parent `text` buffer that this element occupies.
|
||
pub byte_range: ByteRange,
|
||
/// Optional human-readable placeholder for the element, displayed in the UI.
|
||
placeholder: Option<String>,
|
||
}
|
||
|
||
impl TextElement {
|
||
pub fn new(byte_range: ByteRange, placeholder: Option<String>) -> Self {
|
||
Self {
|
||
byte_range,
|
||
placeholder,
|
||
}
|
||
}
|
||
|
||
/// Returns a copy of this element with a remapped byte range.
|
||
///
|
||
/// The placeholder is preserved as-is; callers must ensure the new range
|
||
/// still refers to the same logical element (and same placeholder)
|
||
/// within the new text.
|
||
pub fn map_range<F>(&self, map: F) -> Self
|
||
where
|
||
F: FnOnce(ByteRange) -> ByteRange,
|
||
{
|
||
Self {
|
||
byte_range: map(self.byte_range),
|
||
placeholder: self.placeholder.clone(),
|
||
}
|
||
}
|
||
|
||
pub fn set_placeholder(&mut self, placeholder: Option<String>) {
|
||
self.placeholder = placeholder;
|
||
}
|
||
|
||
/// Returns the stored placeholder without falling back to the text buffer.
|
||
///
|
||
/// This must only be used inside `From<TextElement>` implementations on equivalent
|
||
/// protocol types where the source text is unavailable. Prefer `placeholder(text)`
|
||
/// everywhere else.
|
||
#[doc(hidden)]
|
||
pub fn _placeholder_for_conversion_only(&self) -> Option<&str> {
|
||
self.placeholder.as_deref()
|
||
}
|
||
|
||
pub fn placeholder<'a>(&'a self, text: &'a str) -> Option<&'a str> {
|
||
self.placeholder
|
||
.as_deref()
|
||
.or_else(|| text.get(self.byte_range.start..self.byte_range.end))
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)]
|
||
pub struct ByteRange {
|
||
/// Start byte offset (inclusive) within the UTF-8 text buffer.
|
||
pub start: usize,
|
||
/// End byte offset (exclusive) within the UTF-8 text buffer.
|
||
pub end: usize,
|
||
}
|
||
|
||
impl From<std::ops::Range<usize>> for ByteRange {
|
||
fn from(range: std::ops::Range<usize>) -> Self {
|
||
Self {
|
||
start: range.start,
|
||
end: range.end,
|
||
}
|
||
}
|
||
}
|