diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index a3545fec..9cb0f52c 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -277,13 +277,15 @@ pub async fn chat_message( vec![] }; + // 8 KB embed limit; detect + redact on full content so PII at the boundary is caught. + const EMBED_LIMIT: usize = 8_000; + let mut msg = base; for (file_name, file_path) in &files { let content = std::fs::read_to_string(file_path).unwrap_or_default(); - let preview = &content[..content.len().min(8000)]; - let spans = crate::pii::PiiDetector::new().detect(preview); - let body = if spans.is_empty() { - preview.to_string() + let spans = crate::pii::PiiDetector::new().detect(&content); + let redacted = if spans.is_empty() { + content } else { let types: std::collections::HashSet<&str> = spans.iter().map(|s| s.pii_type.as_str()).collect(); @@ -295,9 +297,22 @@ pub async fn chat_message( pii_count = spans.len(), "PII detected in chat attachment — auto-redacting before AI send" ); - crate::pii::apply_redactions(preview, &spans) + crate::pii::apply_redactions(&content, &spans) }; - msg.push_str(&format!("\n\n--- Attached: {} ---\n{}", file_name, body)); + // Truncate after redaction so the cut never lands inside a PII span. + let embed_end = if redacted.len() > EMBED_LIMIT { + let mut e = EMBED_LIMIT; + while !redacted.is_char_boundary(e) { + e -= 1; + } + e + } else { + redacted.len() + }; + msg.push_str(&format!( + "\n\n--- Attached: {file_name} ---\n{}", + &redacted[..embed_end] + )); } msg }; diff --git a/src-tauri/src/commands/analysis.rs b/src-tauri/src/commands/analysis.rs index 16b798f8..f9cbd15c 100644 --- a/src-tauri/src/commands/analysis.rs +++ b/src-tauri/src/commands/analysis.rs @@ -450,10 +450,19 @@ pub async fn detect_pii( }) } +/// Maximum text size accepted by scan_text_for_pii to prevent DoS on large payloads. +const MAX_TEXT_SCAN_BYTES: usize = 32 * 1024; // 32 KB + /// Scan arbitrary text for PII without creating any database records. -/// Used to warn users before sending typed chat messages to AI providers. +/// Used by the backend before sending typed chat messages to AI providers. #[tauri::command] pub async fn scan_text_for_pii(text: String) -> Result { + if text.len() > MAX_TEXT_SCAN_BYTES { + return Err(format!( + "Text too large for inline PII scan ({} bytes; limit {MAX_TEXT_SCAN_BYTES})", + text.len() + )); + } let detector = PiiDetector::new(); let spans = detector.detect(&text); let total_pii_found = spans.len(); diff --git a/src/lib/tauriCommands.ts b/src/lib/tauriCommands.ts index 62583d09..6c03d976 100644 --- a/src/lib/tauriCommands.ts +++ b/src/lib/tauriCommands.ts @@ -283,7 +283,7 @@ export const chatMessageCmd = ( invoke("chat_message", { issueId, message, - logFileIds: logFileIds.length > 0 ? logFileIds : null, + logFileIds: logFileIds.length > 0 ? logFileIds : undefined, providerConfig, systemPrompt: systemPrompt ?? null, }); diff --git a/src/pages/Triage/index.tsx b/src/pages/Triage/index.tsx index d5cc1b0b..46339ed1 100644 --- a/src/pages/Triage/index.tsx +++ b/src/pages/Triage/index.tsx @@ -8,7 +8,6 @@ import { useSessionStore } from "@/stores/sessionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { chatMessageCmd, - detectPiiCmd, getIssueCmd, getIssueMessagesCmd, uploadLogFileCmd, @@ -41,7 +40,6 @@ export default function Triage() { const navigate = useNavigate(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [notice, setNotice] = useState(null); const [pendingFiles, setPendingFiles] = useState([]); // Track the last user message so we can save it as a resolution step when why level advances const lastUserMsgRef = useRef(""); @@ -135,25 +133,6 @@ export default function Triage() { setIsLoading(true); setError(null); - setNotice(null); - - // Pre-send attachment PII scan: surface a notice to the user about what will be - // auto-redacted. The send is NOT blocked — the backend performs the actual redaction. - const piiNotices: string[] = []; - for (const f of pendingFiles) { - try { - const result = await detectPiiCmd(f.logFileId); - if (result.total_pii_found > 0) { - const types = [...new Set(result.detections.map((d) => d.pii_type))].join(", "); - piiNotices.push(`"${f.name}" (${types})`); - } - } catch { - // Non-fatal — backend will still scan before sending to AI - } - } - if (piiNotices.length > 0) { - setNotice(`PII auto-redacted before sending: ${piiNotices.join("; ")}`); - } const fileNames = pendingFiles.map((f) => f.name); const displayContent = @@ -257,11 +236,6 @@ export default function Triage() { - {notice && ( -
- ℹ️ {notice} -
- )} {error && (
{error}