From 1dcfabea9be11fa92916b87713b0e55683d5550a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 19 Sep 2026 12:09:56 +0000 Subject: [PATCH] Cap intent LLM request text at 64 KiB. POST /v1/intent already rejects messages above 64 KiB, but orchestrator hybrid/llm classify embedded in.text wholesale. A large TUI, chat, or event payload became an unbounded provider prompt. Truncate UTF-8-safely with a [truncated] marker, matching the HTTP limit. Co-authored-by: Tyler Reckart --- CHANGELOG.md | 7 ++++++ include/intent.h | 7 ++++++ src/intent.cpp | 17 ++++++++++++- tests/test_intent.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dae12b43..12dc6bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ loosely while pre-1.0 (breaking changes can land on minor bumps). ## [Unreleased] +### Fixed +- **Intent LLM prompt text cap.** `build_llm_user_prompt` now truncates + request text at 64 KiB (UTF-8 safe, with a `[truncated]` marker) so + orchestrator hybrid/llm classify cannot forward an unbounded TUI, + chat, or event payload to the provider. Matches the existing + `POST /v1/intent` message limit. + ## [0.13.4] — 2026-09-10 ### Fixed diff --git a/include/intent.h b/include/intent.h index 89b92568..c7522a9e 100644 --- a/include/intent.h +++ b/include/intent.h @@ -16,12 +16,19 @@ // Distinct from Constitution::MemoryConfig::intent_routing, which only // boosts /mem search types. +#include #include #include #include namespace arbiter { +// HTTP POST /v1/intent already rejects messages above this size. The +// orchestrator ingress path does not, so the LLM prompt builder must +// truncate here or a large TUI / chat / event payload is forwarded +// wholesale to the provider. +inline constexpr std::size_t kIntentLlmTextMaxBytes = 64 * 1024; + struct IntentConfig { // off | heuristic | hybrid | llm // File-backed agents default off; master_constitution() sets hybrid. diff --git a/src/intent.cpp b/src/intent.cpp index 338e89fd..56767f7d 100644 --- a/src/intent.cpp +++ b/src/intent.cpp @@ -231,9 +231,24 @@ std::vector cue_kinds(const std::string& norm) { return kinds; } +std::string cap_intent_llm_text(std::string text) { + if (text.size() <= kIntentLlmTextMaxBytes) return text; + static constexpr char kMark[] = "\n[truncated]"; + constexpr std::size_t kMarkLen = sizeof(kMark) - 1; + std::size_t keep = (kIntentLlmTextMaxBytes > kMarkLen) + ? (kIntentLlmTextMaxBytes - kMarkLen) : 0; + while (keep > 0 && + (static_cast(text[keep]) & 0xC0) == 0x80) { + --keep; + } + text.resize(keep); + text += kMark; + return text; +} + std::string build_llm_user_prompt(const IntentInput& in, const Intent& hint) { std::ostringstream q; - q << "[REQUEST]\n" << in.text << "\n[END REQUEST]\n\n"; + q << "[REQUEST]\n" << cap_intent_llm_text(in.text) << "\n[END REQUEST]\n\n"; q << "[ROSTER]\n"; if (in.roster.empty()) { q << "(none)\n"; diff --git a/tests/test_intent.cpp b/tests/test_intent.cpp index 28fafc23..fbf73d67 100644 --- a/tests/test_intent.cpp +++ b/tests/test_intent.cpp @@ -356,6 +356,62 @@ TEST_CASE("standalone classify defaults to heuristic, keeps master thresholds") CHECK(cfg.model == "advisor-model"); } +TEST_CASE("llm prompt truncates request text at 64 KiB") { + IntentInput in; + in.text.assign(kIntentLlmTextMaxBytes + 4096, 'x'); + in.requested_agent = "index"; + in.roster = starter_roster(); + IntentConfig cfg; + cfg.mode = "llm"; + + std::string prompt; + auto llm = [&](const std::string& p) { + prompt = p; + return std::string{}; + }; + auto out = resolve_intent(in, cfg, llm); + CHECK(out.llm_used); + REQUIRE_FALSE(prompt.empty()); + + const auto req = prompt.find("[REQUEST]\n"); + const auto end = prompt.find("\n[END REQUEST]"); + REQUIRE(req != std::string::npos); + REQUIRE(end != std::string::npos); + REQUIRE(end > req); + const std::string body = prompt.substr( + req + std::string("[REQUEST]\n").size(), + end - (req + std::string("[REQUEST]\n").size())); + CHECK(body.size() <= kIntentLlmTextMaxBytes); + CHECK(body.find("[truncated]") != std::string::npos); + CHECK(body.find(std::string(kIntentLlmTextMaxBytes, 'x')) == std::string::npos); +} + +TEST_CASE("llm prompt keeps request text at the 64 KiB boundary") { + IntentInput in; + in.text.assign(kIntentLlmTextMaxBytes, 'y'); + in.requested_agent = "index"; + in.roster = starter_roster(); + IntentConfig cfg; + cfg.mode = "llm"; + + std::string prompt; + auto llm = [&](const std::string& p) { + prompt = p; + return std::string{}; + }; + resolve_intent(in, cfg, llm); + + const auto req = prompt.find("[REQUEST]\n"); + const auto end = prompt.find("\n[END REQUEST]"); + REQUIRE(req != std::string::npos); + REQUIRE(end != std::string::npos); + const std::string body = prompt.substr( + req + std::string("[REQUEST]\n").size(), + end - (req + std::string("[REQUEST]\n").size())); + CHECK(body == in.text); + CHECK(body.find("[truncated]") == std::string::npos); +} + TEST_CASE("standalone heuristic does not invoke llm on unconfident text") { IntentInput in; in.text = "Hello there, please help";